diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index bd7a5f1ad..39e0c5960 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -17,10 +17,8 @@ */ package org.jackhuang.hmcl.setting; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; -import com.google.gson.ToNumberPolicy; +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -38,18 +36,21 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer; import org.jackhuang.hmcl.util.gson.FileTypeAdapter; +import org.jackhuang.hmcl.util.gson.ObservableField; import org.jackhuang.hmcl.util.gson.PaintAdapter; import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; +import org.jackhuang.hmcl.util.javafx.DirtyTracker; import org.jackhuang.hmcl.util.javafx.ObservableHelper; -import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.*; import java.net.Proxy; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; +@JsonAdapter(value = Config.Adapter.class) public final class Config implements Observable { public static final int CURRENT_UI_VERSION = 0; @@ -67,160 +68,44 @@ public final class Config implements Observable { .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .create(); - @Nullable - public static Config fromJson(String json) throws JsonParseException { - Config loaded = CONFIG_GSON.fromJson(json, Config.class); - if (loaded == null) { - return null; + private static final List> FIELDS; + + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + Field[] fields = Config.class.getDeclaredFields(); + + var configFields = new ArrayList>(fields.length); + for (Field field : fields) { + int modifiers = field.getModifiers(); + if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)) + continue; + + configFields.add(ObservableField.of(lookup, field)); } - Config instance = new Config(); - PropertyUtils.copyProperties(loaded, instance); - return instance; + FIELDS = List.copyOf(configFields); } - @SerializedName("last") - private StringProperty selectedProfile = new SimpleStringProperty(""); + @Nullable + public static Config fromJson(String json) throws JsonParseException { + return CONFIG_GSON.fromJson(json, Config.class); + } - @SerializedName("backgroundType") - private ObjectProperty backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT); - - @SerializedName("bgpath") - private StringProperty backgroundImage = new SimpleStringProperty(); - - @SerializedName("bgurl") - private StringProperty backgroundImageUrl = new SimpleStringProperty(); - - @SerializedName("bgImageOpacity") - private IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100); - - @SerializedName("bgpaint") - private ObjectProperty backgroundPaint = new SimpleObjectProperty<>(); - - @SerializedName("commonDirType") - private ObjectProperty commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT); - - @SerializedName("commonpath") - private StringProperty commonDirectory = new SimpleStringProperty(Metadata.MINECRAFT_DIRECTORY.toString()); - - @SerializedName("hasProxy") - private BooleanProperty hasProxy = new SimpleBooleanProperty(); - - @SerializedName("hasProxyAuth") - private BooleanProperty hasProxyAuth = new SimpleBooleanProperty(); - - @SerializedName("proxyType") - private ObjectProperty proxyType = new SimpleObjectProperty<>(Proxy.Type.HTTP); - - @SerializedName("proxyHost") - private StringProperty proxyHost = new SimpleStringProperty(); - - @SerializedName("proxyPort") - private IntegerProperty proxyPort = new SimpleIntegerProperty(); - - @SerializedName("proxyUserName") - private StringProperty proxyUser = new SimpleStringProperty(); - - @SerializedName("proxyPassword") - private StringProperty proxyPass = new SimpleStringProperty(); - - @SerializedName("x") - private DoubleProperty x = new SimpleDoubleProperty(); - - @SerializedName("y") - private DoubleProperty y = new SimpleDoubleProperty(); - - @SerializedName("width") - private DoubleProperty width = new SimpleDoubleProperty(); - - @SerializedName("height") - private DoubleProperty height = new SimpleDoubleProperty(); - - @SerializedName("theme") - private ObjectProperty theme = new SimpleObjectProperty<>(); - - @SerializedName("localization") - private ObjectProperty localization = new SimpleObjectProperty<>(Locales.DEFAULT); - - @SerializedName("autoDownloadThreads") - private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true); - - @SerializedName("downloadThreads") - private IntegerProperty downloadThreads = new SimpleIntegerProperty(64); - - @SerializedName("downloadType") - private StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID); - - @SerializedName("autoChooseDownloadType") - private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true); - - @SerializedName("versionListSource") - private StringProperty versionListSource = new SimpleStringProperty("balanced"); - - @SerializedName("configurations") - private SimpleMapProperty configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>())); - - @SerializedName("selectedAccount") - private StringProperty selectedAccount = new SimpleStringProperty(); - - @SerializedName("accounts") - private ObservableList> accountStorages = FXCollections.observableArrayList(); - - @SerializedName("fontFamily") - private StringProperty fontFamily = new SimpleStringProperty(); - - @SerializedName("fontSize") - private DoubleProperty fontSize = new SimpleDoubleProperty(12); - - @SerializedName("launcherFontFamily") - private StringProperty launcherFontFamily = new SimpleStringProperty(); - - @SerializedName("logLines") - private ObjectProperty logLines = new SimpleObjectProperty<>(); - - @SerializedName("titleTransparent") - private BooleanProperty titleTransparent = new SimpleBooleanProperty(false); - - @SerializedName("authlibInjectorServers") - private ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server}); - - @SerializedName("addedLittleSkin") - private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false); - - @SerializedName("disableAutoGameOptions") - private BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false); - - @SerializedName("promptedVersion") - private StringProperty promptedVersion = new SimpleStringProperty(); - - @SerializedName("_version") - private IntegerProperty configVersion = new SimpleIntegerProperty(0); - - /** - * The version of UI that the user have last used. - * If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased. - * When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown, - * then this property is set to the same value as {@link #CURRENT_UI_VERSION}. - * In particular, the property is default to 0, so that whoever open the application for the first time will see the guide. - */ - @SerializedName("uiVersion") - private IntegerProperty uiVersion = new SimpleIntegerProperty(0); - - /** - * The preferred login type to use when the user wants to add an account. - */ - @SerializedName("preferredLoginType") - private StringProperty preferredLoginType = new SimpleStringProperty(); - - @SerializedName("animationDisabled") - private BooleanProperty animationDisabled = new SimpleBooleanProperty(); - - @SerializedName("shownTips") - private ObservableMap shownTips = FXCollections.observableHashMap(); - - private transient ObservableHelper helper = new ObservableHelper(this); + private transient final ObservableHelper helper = new ObservableHelper(this); + private transient final DirtyTracker tracker = new DirtyTracker(); + private transient final Map unknownFields = new HashMap<>(); public Config() { - PropertyUtils.attachListener(this, helper); + var shouldBeWrite = Collections.newSetFromMap(new IdentityHashMap<>()); + Collections.addAll(shouldBeWrite, configVersion, uiVersion); + + for (var field : FIELDS) { + Observable observable = field.get(this); + if (shouldBeWrite.contains(observable)) + tracker.markDirty(observable); + else + tracker.track(observable); + observable.addListener(helper); + } } @Override @@ -237,245 +122,110 @@ public final class Config implements Observable { return CONFIG_GSON.toJson(this); } - // Getters & Setters & Properties - public String getSelectedProfile() { - return selectedProfile.get(); + // Properties + + @SerializedName("_version") + private final IntegerProperty configVersion = new SimpleIntegerProperty(0); + + public IntegerProperty configVersionProperty() { + return configVersion; } - public void setSelectedProfile(String selectedProfile) { - this.selectedProfile.set(selectedProfile); + public int getConfigVersion() { + return configVersion.get(); } - public StringProperty selectedProfileProperty() { - return selectedProfile; + public void setConfigVersion(int configVersion) { + this.configVersion.set(configVersion); } - public EnumBackgroundImage getBackgroundImageType() { - return backgroundImageType.get(); + /** + * The version of UI that the user have last used. + * If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased. + * When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown, + * then this property is set to the same value as {@link #CURRENT_UI_VERSION}. + * In particular, the property is default to 0, so that whoever open the application for the first time will see the guide. + */ + @SerializedName("uiVersion") + private final IntegerProperty uiVersion = new SimpleIntegerProperty(CURRENT_UI_VERSION); + + public IntegerProperty uiVersionProperty() { + return uiVersion; } - public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) { - this.backgroundImageType.set(backgroundImageType); + public int getUiVersion() { + return uiVersion.get(); } - public ObjectProperty backgroundImageTypeProperty() { - return backgroundImageType; + public void setUiVersion(int uiVersion) { + this.uiVersion.set(uiVersion); } - public String getBackgroundImage() { - return backgroundImage.get(); - } + @SerializedName("x") + private final DoubleProperty x = new SimpleDoubleProperty(); - public void setBackgroundImage(String backgroundImage) { - this.backgroundImage.set(backgroundImage); - } - - public StringProperty backgroundImageProperty() { - return backgroundImage; - } - - public String getBackgroundImageUrl() { - return backgroundImageUrl.get(); - } - - public StringProperty backgroundImageUrlProperty() { - return backgroundImageUrl; - } - - public void setBackgroundImageUrl(String backgroundImageUrl) { - this.backgroundImageUrl.set(backgroundImageUrl); - } - - public Paint getBackgroundPaint() { - return backgroundPaint.get(); - } - - public ObjectProperty backgroundPaintProperty() { - return backgroundPaint; - } - - public void setBackgroundPaint(Paint backgroundPaint) { - this.backgroundPaint.set(backgroundPaint); - } - - public int getBackgroundImageOpacity() { - return backgroundImageOpacity.get(); - } - - public void setBackgroundImageOpacity(int backgroundImageOpacity) { - this.backgroundImageOpacity.set(backgroundImageOpacity); - } - - public IntegerProperty backgroundImageOpacityProperty() { - return backgroundImageOpacity; - } - - public EnumCommonDirectory getCommonDirType() { - return commonDirType.get(); - } - - public ObjectProperty commonDirTypeProperty() { - return commonDirType; - } - - public void setCommonDirType(EnumCommonDirectory commonDirType) { - this.commonDirType.set(commonDirType); - } - - public String getCommonDirectory() { - return commonDirectory.get(); - } - - public void setCommonDirectory(String commonDirectory) { - this.commonDirectory.set(commonDirectory); - } - - public StringProperty commonDirectoryProperty() { - return commonDirectory; - } - - public boolean hasProxy() { - return hasProxy.get(); - } - - public void setHasProxy(boolean hasProxy) { - this.hasProxy.set(hasProxy); - } - - public BooleanProperty hasProxyProperty() { - return hasProxy; - } - - public boolean hasProxyAuth() { - return hasProxyAuth.get(); - } - - public void setHasProxyAuth(boolean hasProxyAuth) { - this.hasProxyAuth.set(hasProxyAuth); - } - - public BooleanProperty hasProxyAuthProperty() { - return hasProxyAuth; - } - - public Proxy.Type getProxyType() { - return proxyType.get(); - } - - public void setProxyType(Proxy.Type proxyType) { - this.proxyType.set(proxyType); - } - - public ObjectProperty proxyTypeProperty() { - return proxyType; - } - - public String getProxyHost() { - return proxyHost.get(); - } - - public void setProxyHost(String proxyHost) { - this.proxyHost.set(proxyHost); - } - - public StringProperty proxyHostProperty() { - return proxyHost; - } - - public int getProxyPort() { - return proxyPort.get(); - } - - public void setProxyPort(int proxyPort) { - this.proxyPort.set(proxyPort); - } - - public IntegerProperty proxyPortProperty() { - return proxyPort; - } - - public String getProxyUser() { - return proxyUser.get(); - } - - public void setProxyUser(String proxyUser) { - this.proxyUser.set(proxyUser); - } - - public StringProperty proxyUserProperty() { - return proxyUser; - } - - public String getProxyPass() { - return proxyPass.get(); - } - - public void setProxyPass(String proxyPass) { - this.proxyPass.set(proxyPass); - } - - public StringProperty proxyPassProperty() { - return proxyPass; + public DoubleProperty xProperty() { + return x; } public double getX() { return x.get(); } - public DoubleProperty xProperty() { - return x; - } - public void setX(double x) { this.x.set(x); } + @SerializedName("y") + private final DoubleProperty y = new SimpleDoubleProperty(); + + public DoubleProperty yProperty() { + return y; + } + public double getY() { return y.get(); } - public DoubleProperty yProperty() { - return y; - } - public void setY(double y) { this.y.set(y); } + @SerializedName("width") + private final DoubleProperty width = new SimpleDoubleProperty(); + + public DoubleProperty widthProperty() { + return width; + } + public double getWidth() { return width.get(); } - public DoubleProperty widthProperty() { - return width; - } - public void setWidth(double width) { this.width.set(width); } + @SerializedName("height") + private final DoubleProperty height = new SimpleDoubleProperty(); + + public DoubleProperty heightProperty() { + return height; + } + public double getHeight() { return height.get(); } - public DoubleProperty heightProperty() { - return height; - } - public void setHeight(double height) { this.height.set(height); } - public Theme getTheme() { - return theme.get(); - } + @SerializedName("localization") + private final ObjectProperty localization = new SimpleObjectProperty<>(Locales.DEFAULT); - public void setTheme(Theme theme) { - this.theme.set(theme); - } - - public ObjectProperty themeProperty() { - return theme; + public ObjectProperty localizationProperty() { + return localization; } public SupportedLocale getLocalization() { @@ -486,225 +236,8 @@ public final class Config implements Observable { this.localization.set(localization); } - public ObjectProperty localizationProperty() { - return localization; - } - - public boolean getAutoDownloadThreads() { - return autoDownloadThreads.get(); - } - - public BooleanProperty autoDownloadThreadsProperty() { - return autoDownloadThreads; - } - - public void setAutoDownloadThreads(boolean autoDownloadThreads) { - this.autoDownloadThreads.set(autoDownloadThreads); - } - - public int getDownloadThreads() { - return downloadThreads.get(); - } - - public IntegerProperty downloadThreadsProperty() { - return downloadThreads; - } - - public void setDownloadThreads(int downloadThreads) { - this.downloadThreads.set(downloadThreads); - } - - public String getDownloadType() { - return downloadType.get(); - } - - public void setDownloadType(String downloadType) { - this.downloadType.set(downloadType); - } - - public StringProperty downloadTypeProperty() { - return downloadType; - } - - public boolean isAutoChooseDownloadType() { - return autoChooseDownloadType.get(); - } - - public BooleanProperty autoChooseDownloadTypeProperty() { - return autoChooseDownloadType; - } - - public void setAutoChooseDownloadType(boolean autoChooseDownloadType) { - this.autoChooseDownloadType.set(autoChooseDownloadType); - } - - public String getVersionListSource() { - return versionListSource.get(); - } - - public void setVersionListSource(String versionListSource) { - this.versionListSource.set(versionListSource); - } - - public StringProperty versionListSourceProperty() { - return versionListSource; - } - - public MapProperty getConfigurations() { - return configurations; - } - - public String getSelectedAccount() { - return selectedAccount.get(); - } - - public void setSelectedAccount(String selectedAccount) { - this.selectedAccount.set(selectedAccount); - } - - public StringProperty selectedAccountProperty() { - return selectedAccount; - } - - public ObservableList> getAccountStorages() { - return accountStorages; - } - - public String getFontFamily() { - return fontFamily.get(); - } - - public void setFontFamily(String fontFamily) { - this.fontFamily.set(fontFamily); - } - - public StringProperty fontFamilyProperty() { - return fontFamily; - } - - public double getFontSize() { - return fontSize.get(); - } - - public void setFontSize(double fontSize) { - this.fontSize.set(fontSize); - } - - public DoubleProperty fontSizeProperty() { - return fontSize; - } - - public String getLauncherFontFamily() { - return launcherFontFamily.get(); - } - - public StringProperty launcherFontFamilyProperty() { - return launcherFontFamily; - } - - public void setLauncherFontFamily(String launcherFontFamily) { - this.launcherFontFamily.set(launcherFontFamily); - } - - public Integer getLogLines() { - return logLines.get(); - } - - public void setLogLines(Integer logLines) { - this.logLines.set(logLines); - } - - public ObjectProperty logLinesProperty() { - return logLines; - } - - public ObservableList getAuthlibInjectorServers() { - return authlibInjectorServers; - } - - public boolean isAddedLittleSkin() { - return addedLittleSkin.get(); - } - - public BooleanProperty addedLittleSkinProperty() { - return addedLittleSkin; - } - - public void setAddedLittleSkin(boolean addedLittleSkin) { - this.addedLittleSkin.set(addedLittleSkin); - } - - public BooleanProperty disableAutoGameOptionsProperty() { - return disableAutoGameOptions; - } - - public boolean isDisableAutoGameOptions() { - return disableAutoGameOptions.get(); - } - - public void setDisableAutoGameOptions(boolean disableAutoGameOptions) { - this.disableAutoGameOptions.set(disableAutoGameOptions); - } - - public int getConfigVersion() { - return configVersion.get(); - } - - public IntegerProperty configVersionProperty() { - return configVersion; - } - - public void setConfigVersion(int configVersion) { - this.configVersion.set(configVersion); - } - - public int getUiVersion() { - return uiVersion.get(); - } - - public IntegerProperty uiVersionProperty() { - return uiVersion; - } - - public void setUiVersion(int uiVersion) { - this.uiVersion.set(uiVersion); - } - - public String getPreferredLoginType() { - return preferredLoginType.get(); - } - - public void setPreferredLoginType(String preferredLoginType) { - this.preferredLoginType.set(preferredLoginType); - } - - public StringProperty preferredLoginTypeProperty() { - return preferredLoginType; - } - - public boolean isAnimationDisabled() { - return animationDisabled.get(); - } - - public BooleanProperty animationDisabledProperty() { - return animationDisabled; - } - - public void setAnimationDisabled(boolean animationDisabled) { - this.animationDisabled.set(animationDisabled); - } - - public boolean isTitleTransparent() { - return titleTransparent.get(); - } - - public BooleanProperty titleTransparentProperty() { - return titleTransparent; - } - - public void setTitleTransparent(boolean titleTransparent) { - this.titleTransparent.set(titleTransparent); - } + @SerializedName("promptedVersion") + private final StringProperty promptedVersion = new SimpleStringProperty(); public String getPromptedVersion() { return promptedVersion.get(); @@ -718,7 +251,553 @@ public final class Config implements Observable { this.promptedVersion.set(promptedVersion); } + @SerializedName("shownTips") + private final ObservableMap shownTips = FXCollections.observableHashMap(); + public ObservableMap getShownTips() { return shownTips; } + + @SerializedName("commonDirType") + private final ObjectProperty commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT); + + public ObjectProperty commonDirTypeProperty() { + return commonDirType; + } + + public EnumCommonDirectory getCommonDirType() { + return commonDirType.get(); + } + + public void setCommonDirType(EnumCommonDirectory commonDirType) { + this.commonDirType.set(commonDirType); + } + + @SerializedName("commonpath") + private final StringProperty commonDirectory = new SimpleStringProperty(Metadata.MINECRAFT_DIRECTORY.toString()); + + public StringProperty commonDirectoryProperty() { + return commonDirectory; + } + + public String getCommonDirectory() { + return commonDirectory.get(); + } + + public void setCommonDirectory(String commonDirectory) { + this.commonDirectory.set(commonDirectory); + } + + @SerializedName("logLines") + private final ObjectProperty logLines = new SimpleObjectProperty<>(); + + public ObjectProperty logLinesProperty() { + return logLines; + } + + public Integer getLogLines() { + return logLines.get(); + } + + public void setLogLines(Integer logLines) { + this.logLines.set(logLines); + } + + // UI + + @SerializedName("theme") + private final ObjectProperty theme = new SimpleObjectProperty<>(); + + public ObjectProperty themeProperty() { + return theme; + } + + public Theme getTheme() { + return theme.get(); + } + + public void setTheme(Theme theme) { + this.theme.set(theme); + } + + @SerializedName("fontFamily") + private final StringProperty fontFamily = new SimpleStringProperty(); + + public StringProperty fontFamilyProperty() { + return fontFamily; + } + + public String getFontFamily() { + return fontFamily.get(); + } + + public void setFontFamily(String fontFamily) { + this.fontFamily.set(fontFamily); + } + + @SerializedName("fontSize") + private final DoubleProperty fontSize = new SimpleDoubleProperty(12); + + public DoubleProperty fontSizeProperty() { + return fontSize; + } + + public double getFontSize() { + return fontSize.get(); + } + + public void setFontSize(double fontSize) { + this.fontSize.set(fontSize); + } + + @SerializedName("launcherFontFamily") + private final StringProperty launcherFontFamily = new SimpleStringProperty(); + + public StringProperty launcherFontFamilyProperty() { + return launcherFontFamily; + } + + public String getLauncherFontFamily() { + return launcherFontFamily.get(); + } + + public void setLauncherFontFamily(String launcherFontFamily) { + this.launcherFontFamily.set(launcherFontFamily); + } + + @SerializedName("animationDisabled") + private final BooleanProperty animationDisabled = new SimpleBooleanProperty(); + + public BooleanProperty animationDisabledProperty() { + return animationDisabled; + } + + public boolean isAnimationDisabled() { + return animationDisabled.get(); + } + + public void setAnimationDisabled(boolean animationDisabled) { + this.animationDisabled.set(animationDisabled); + } + + @SerializedName("titleTransparent") + private final BooleanProperty titleTransparent = new SimpleBooleanProperty(false); + + public BooleanProperty titleTransparentProperty() { + return titleTransparent; + } + + public boolean isTitleTransparent() { + return titleTransparent.get(); + } + + public void setTitleTransparent(boolean titleTransparent) { + this.titleTransparent.set(titleTransparent); + } + + @SerializedName("backgroundType") + private final ObjectProperty backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT); + + public ObjectProperty backgroundImageTypeProperty() { + return backgroundImageType; + } + + public EnumBackgroundImage getBackgroundImageType() { + return backgroundImageType.get(); + } + + public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) { + this.backgroundImageType.set(backgroundImageType); + } + + @SerializedName("bgpath") + private final StringProperty backgroundImage = new SimpleStringProperty(); + + public StringProperty backgroundImageProperty() { + return backgroundImage; + } + + public String getBackgroundImage() { + return backgroundImage.get(); + } + + public void setBackgroundImage(String backgroundImage) { + this.backgroundImage.set(backgroundImage); + } + + @SerializedName("bgurl") + private final StringProperty backgroundImageUrl = new SimpleStringProperty(); + + public StringProperty backgroundImageUrlProperty() { + return backgroundImageUrl; + } + + public String getBackgroundImageUrl() { + return backgroundImageUrl.get(); + } + + public void setBackgroundImageUrl(String backgroundImageUrl) { + this.backgroundImageUrl.set(backgroundImageUrl); + } + + @SerializedName("bgpaint") + private final ObjectProperty backgroundPaint = new SimpleObjectProperty<>(); + + public Paint getBackgroundPaint() { + return backgroundPaint.get(); + } + + public ObjectProperty backgroundPaintProperty() { + return backgroundPaint; + } + + public void setBackgroundPaint(Paint backgroundPaint) { + this.backgroundPaint.set(backgroundPaint); + } + + @SerializedName("bgImageOpacity") + private final IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100); + + public IntegerProperty backgroundImageOpacityProperty() { + return backgroundImageOpacity; + } + + public int getBackgroundImageOpacity() { + return backgroundImageOpacity.get(); + } + + public void setBackgroundImageOpacity(int backgroundImageOpacity) { + this.backgroundImageOpacity.set(backgroundImageOpacity); + } + + // Networks + + @SerializedName("autoDownloadThreads") + private final BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true); + + public BooleanProperty autoDownloadThreadsProperty() { + return autoDownloadThreads; + } + + public boolean getAutoDownloadThreads() { + return autoDownloadThreads.get(); + } + + public void setAutoDownloadThreads(boolean autoDownloadThreads) { + this.autoDownloadThreads.set(autoDownloadThreads); + } + + @SerializedName("downloadThreads") + private final IntegerProperty downloadThreads = new SimpleIntegerProperty(64); + + public IntegerProperty downloadThreadsProperty() { + return downloadThreads; + } + + public int getDownloadThreads() { + return downloadThreads.get(); + } + + public void setDownloadThreads(int downloadThreads) { + this.downloadThreads.set(downloadThreads); + } + + @SerializedName("downloadType") + private final StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID); + + public StringProperty downloadTypeProperty() { + return downloadType; + } + + public String getDownloadType() { + return downloadType.get(); + } + + public void setDownloadType(String downloadType) { + this.downloadType.set(downloadType); + } + + @SerializedName("autoChooseDownloadType") + private final BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true); + + public BooleanProperty autoChooseDownloadTypeProperty() { + return autoChooseDownloadType; + } + + public boolean isAutoChooseDownloadType() { + return autoChooseDownloadType.get(); + } + + public void setAutoChooseDownloadType(boolean autoChooseDownloadType) { + this.autoChooseDownloadType.set(autoChooseDownloadType); + } + + @SerializedName("versionListSource") + private final StringProperty versionListSource = new SimpleStringProperty("balanced"); + + public StringProperty versionListSourceProperty() { + return versionListSource; + } + + public String getVersionListSource() { + return versionListSource.get(); + } + + public void setVersionListSource(String versionListSource) { + this.versionListSource.set(versionListSource); + } + + @SerializedName("hasProxy") + private final BooleanProperty hasProxy = new SimpleBooleanProperty(); + + public BooleanProperty hasProxyProperty() { + return hasProxy; + } + + public boolean hasProxy() { + return hasProxy.get(); + } + + public void setHasProxy(boolean hasProxy) { + this.hasProxy.set(hasProxy); + } + + @SerializedName("hasProxyAuth") + private final BooleanProperty hasProxyAuth = new SimpleBooleanProperty(); + + public BooleanProperty hasProxyAuthProperty() { + return hasProxyAuth; + } + + public boolean hasProxyAuth() { + return hasProxyAuth.get(); + } + + public void setHasProxyAuth(boolean hasProxyAuth) { + this.hasProxyAuth.set(hasProxyAuth); + } + + @SerializedName("proxyType") + private final ObjectProperty proxyType = new SimpleObjectProperty<>(Proxy.Type.HTTP); + + public ObjectProperty proxyTypeProperty() { + return proxyType; + } + + public Proxy.Type getProxyType() { + return proxyType.get(); + } + + public void setProxyType(Proxy.Type proxyType) { + this.proxyType.set(proxyType); + } + + @SerializedName("proxyHost") + private final StringProperty proxyHost = new SimpleStringProperty(); + + public StringProperty proxyHostProperty() { + return proxyHost; + } + + public String getProxyHost() { + return proxyHost.get(); + } + + public void setProxyHost(String proxyHost) { + this.proxyHost.set(proxyHost); + } + + @SerializedName("proxyPort") + private final IntegerProperty proxyPort = new SimpleIntegerProperty(); + + public IntegerProperty proxyPortProperty() { + return proxyPort; + } + + public int getProxyPort() { + return proxyPort.get(); + } + + public void setProxyPort(int proxyPort) { + this.proxyPort.set(proxyPort); + } + + @SerializedName("proxyUserName") + private final StringProperty proxyUser = new SimpleStringProperty(); + + public StringProperty proxyUserProperty() { + return proxyUser; + } + + public String getProxyUser() { + return proxyUser.get(); + } + + public void setProxyUser(String proxyUser) { + this.proxyUser.set(proxyUser); + } + + @SerializedName("proxyPassword") + private final StringProperty proxyPass = new SimpleStringProperty(); + + public StringProperty proxyPassProperty() { + return proxyPass; + } + + public String getProxyPass() { + return proxyPass.get(); + } + + public void setProxyPass(String proxyPass) { + this.proxyPass.set(proxyPass); + } + + // Game + + @SerializedName("disableAutoGameOptions") + private final BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false); + + public BooleanProperty disableAutoGameOptionsProperty() { + return disableAutoGameOptions; + } + + public boolean isDisableAutoGameOptions() { + return disableAutoGameOptions.get(); + } + + public void setDisableAutoGameOptions(boolean disableAutoGameOptions) { + this.disableAutoGameOptions.set(disableAutoGameOptions); + } + + // Accounts + + @SerializedName("authlibInjectorServers") + private final ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server}); + + public ObservableList getAuthlibInjectorServers() { + return authlibInjectorServers; + } + + @SerializedName("addedLittleSkin") + private final BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false); + + public BooleanProperty addedLittleSkinProperty() { + return addedLittleSkin; + } + + public boolean isAddedLittleSkin() { + return addedLittleSkin.get(); + } + + public void setAddedLittleSkin(boolean addedLittleSkin) { + this.addedLittleSkin.set(addedLittleSkin); + } + + /** + * The preferred login type to use when the user wants to add an account. + */ + @SerializedName("preferredLoginType") + private final StringProperty preferredLoginType = new SimpleStringProperty(); + + public StringProperty preferredLoginTypeProperty() { + return preferredLoginType; + } + + public String getPreferredLoginType() { + return preferredLoginType.get(); + } + + public void setPreferredLoginType(String preferredLoginType) { + this.preferredLoginType.set(preferredLoginType); + } + + @SerializedName("selectedAccount") + private final StringProperty selectedAccount = new SimpleStringProperty(); + + public StringProperty selectedAccountProperty() { + return selectedAccount; + } + + public String getSelectedAccount() { + return selectedAccount.get(); + } + + public void setSelectedAccount(String selectedAccount) { + this.selectedAccount.set(selectedAccount); + } + + @SerializedName("accounts") + private final ObservableList> accountStorages = FXCollections.observableArrayList(); + + public ObservableList> getAccountStorages() { + return accountStorages; + } + + // Configurations + + @SerializedName("last") + private final StringProperty selectedProfile = new SimpleStringProperty(""); + + public StringProperty selectedProfileProperty() { + return selectedProfile; + } + + public String getSelectedProfile() { + return selectedProfile.get(); + } + + public void setSelectedProfile(String selectedProfile) { + this.selectedProfile.set(selectedProfile); + } + + @SerializedName("configurations") + private final SimpleMapProperty configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>())); + + public MapProperty getConfigurations() { + return configurations; + } + + public static final class Adapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Config config, Type typeOfSrc, JsonSerializationContext context) { + if (config == null) + return JsonNull.INSTANCE; + + JsonObject result = new JsonObject(); + for (var field : FIELDS) { + Observable observable = field.get(config); + if (config.tracker.isDirty(observable)) { + JsonElement serialized = field.serialize(config, context); + if (serialized != null && !serialized.isJsonNull()) + result.add(field.getSerializedName(), serialized); + } + } + config.unknownFields.forEach(result::add); + return result; + } + + @Override + public Config deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json == null || json.isJsonNull()) + return null; + + if (!json.isJsonObject()) + throw new JsonParseException("Config is not an object: " + json); + + Config config = new Config(); + + var values = new LinkedHashMap<>(json.getAsJsonObject().asMap()); + for (ObservableField field : FIELDS) { + JsonElement value = values.remove(field.getSerializedName()); + if (value != null) { + config.tracker.markDirty(field.get(config)); + field.deserialize(config, value, context); + } + } + + config.unknownFields.putAll(values); + return config; + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/TypeUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/TypeUtils.java new file mode 100644 index 000000000..01d3e0467 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/TypeUtils.java @@ -0,0 +1,690 @@ +// Copy from GsonTypes + +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jackhuang.hmcl.util; + +import static com.google.gson.internal.GsonPreconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Properties; + +/** + * Static methods for working with types. + * + * @author Bob Lee + * @author Jesse Wilson + */ +@SuppressWarnings("MemberName") // legacy class name +public final class TypeUtils { + static final Type[] EMPTY_TYPE_ARRAY = new Type[]{}; + + private TypeUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType} and + * enclosed by {@code ownerType}. + * + * @return a {@link java.io.Serializable serializable} parameterized type. + */ + public static ParameterizedType newParameterizedTypeWithOwner( + Type ownerType, Class rawType, Type... typeArguments) { + return new ParameterizedTypeImpl(ownerType, rawType, typeArguments); + } + + /** + * Returns an array type whose elements are all instances of {@code componentType}. + * + * @return a {@link java.io.Serializable serializable} generic array type. + */ + public static GenericArrayType arrayOf(Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + /** + * Returns a type that represents an unknown type that extends {@code bound}. For example, if + * {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If + * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code ? + * extends Object}. + */ + public static WildcardType subtypeOf(Type bound) { + Type[] upperBounds; + if (bound instanceof WildcardType) { + upperBounds = ((WildcardType) bound).getUpperBounds(); + } else { + upperBounds = new Type[]{bound}; + } + return new WildcardTypeImpl(upperBounds, EMPTY_TYPE_ARRAY); + } + + /** + * Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code + * bound} is {@code String.class}, this returns {@code ? super String}. + */ + public static WildcardType supertypeOf(Type bound) { + Type[] lowerBounds; + if (bound instanceof WildcardType) { + lowerBounds = ((WildcardType) bound).getLowerBounds(); + } else { + lowerBounds = new Type[]{bound}; + } + return new WildcardTypeImpl(new Type[]{Object.class}, lowerBounds); + } + + /** + * Returns a type that is functionally equal but not necessarily equal according to {@link + * Object#equals(Object) Object.equals()}. The returned type is {@link java.io.Serializable}. + */ + public static Type canonicalize(Type type) { + if (type instanceof Class) { + Class c = (Class) type; + return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c; + + } else if (type instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) type; + return new ParameterizedTypeImpl( + p.getOwnerType(), (Class) p.getRawType(), p.getActualTypeArguments()); + + } else if (type instanceof GenericArrayType) { + GenericArrayType g = (GenericArrayType) type; + return new GenericArrayTypeImpl(g.getGenericComponentType()); + + } else if (type instanceof WildcardType) { + WildcardType w = (WildcardType) type; + return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); + + } else { + // type is either serializable as-is or unsupported + return type; + } + } + + public static Class getRawType(Type type) { + if (type instanceof Class) { + // type is a normal class. + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // getRawType() returns Type instead of Class; that seems to be an API mistake, + // see https://bugs.openjdk.org/browse/JDK-8250659 + Type rawType = parameterizedType.getRawType(); + checkArgument(rawType instanceof Class); + return (Class) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // we could use the variable's bounds, but that won't work if there are multiple. + // having a raw type that's more general than necessary is okay + return Object.class; + + } else if (type instanceof WildcardType) { + Type[] bounds = ((WildcardType) type).getUpperBounds(); + // Currently the JLS only permits one bound for wildcards so using first bound is safe + assert bounds.length == 1; + return getRawType(bounds[0]); + + } else { + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException( + "Expected a Class, ParameterizedType, or GenericArrayType, but <" + + type + + "> is of type " + + className); + } + } + + private static boolean equal(Object a, Object b) { + return Objects.equals(a, b); + } + + /** + * Returns true if {@code a} and {@code b} are equal. + */ + public static boolean equals(Type a, Type b) { + if (a == b) { + // also handles (a == null && b == null) + return true; + + } else if (a instanceof Class) { + // Class already specifies equals(). + return a.equals(b); + + } else if (a instanceof ParameterizedType) { + if (!(b instanceof ParameterizedType)) { + return false; + } + + // TODO: save a .clone() call + ParameterizedType pa = (ParameterizedType) a; + ParameterizedType pb = (ParameterizedType) b; + return equal(pa.getOwnerType(), pb.getOwnerType()) + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); + + } else if (a instanceof GenericArrayType) { + if (!(b instanceof GenericArrayType)) { + return false; + } + + GenericArrayType ga = (GenericArrayType) a; + GenericArrayType gb = (GenericArrayType) b; + return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); + + } else if (a instanceof WildcardType) { + if (!(b instanceof WildcardType)) { + return false; + } + + WildcardType wa = (WildcardType) a; + WildcardType wb = (WildcardType) b; + return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + + } else if (a instanceof TypeVariable) { + if (!(b instanceof TypeVariable)) { + return false; + } + TypeVariable va = (TypeVariable) a; + TypeVariable vb = (TypeVariable) b; + return Objects.equals(va.getGenericDeclaration(), vb.getGenericDeclaration()) + && va.getName().equals(vb.getName()); + + } else { + // This isn't a type we support. Could be a generic array type, wildcard type, etc. + return false; + } + } + + public static String typeToString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + /** + * Returns the generic supertype for {@code supertype}. For example, given a class {@code + * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set} and the + * result when the supertype is {@code Collection.class} is {@code Collection}. + */ + public static Type getGenericSupertype(Type context, Class rawType, Class supertype) { + if (supertype == rawType) { + return context; + } + + // we skip searching through interfaces if unknown is an interface + if (supertype.isInterface()) { + Class[] interfaces = rawType.getInterfaces(); + for (int i = 0, length = interfaces.length; i < length; i++) { + if (interfaces[i] == supertype) { + return rawType.getGenericInterfaces()[i]; + } else if (supertype.isAssignableFrom(interfaces[i])) { + return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], supertype); + } + } + } + + // check our supertypes + if (!rawType.isInterface()) { + while (rawType != Object.class) { + Class rawSupertype = rawType.getSuperclass(); + if (rawSupertype == supertype) { + return rawType.getGenericSuperclass(); + } else if (supertype.isAssignableFrom(rawSupertype)) { + return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, supertype); + } + rawType = rawSupertype; + } + } + + // we can't resolve this further + return supertype; + } + + /** + * Returns the generic form of {@code supertype}. For example, if this is {@code + * ArrayList}, this returns {@code Iterable} given the input {@code + * Iterable.class}. + * + * @param supertype a superclass of, or interface implemented by, this. + */ + public static Type getSupertype(Type context, Class contextRawType, Class supertype) { + if (context instanceof WildcardType) { + // Wildcards are useless for resolving supertypes. As the upper bound has the same raw type, + // use it instead + Type[] bounds = ((WildcardType) context).getUpperBounds(); + // Currently the JLS only permits one bound for wildcards so using first bound is safe + assert bounds.length == 1; + context = bounds[0]; + } + checkArgument(supertype.isAssignableFrom(contextRawType)); + return resolve( + context, contextRawType, TypeUtils.getGenericSupertype(context, contextRawType, supertype)); + } + + /** + * Returns the component type of this array type. + * + * @throws ClassCastException if this type is not an array. + */ + public static Type getArrayComponentType(Type array) { + return array instanceof GenericArrayType + ? ((GenericArrayType) array).getGenericComponentType() + : ((Class) array).getComponentType(); + } + + /** + * Returns the element type of this collection type. + * + * @throws IllegalArgumentException if this type is not a collection. + */ + public static Type getCollectionElementType(Type context, Class contextRawType) { + Type collectionType = getSupertype(context, contextRawType, Collection.class); + + if (collectionType instanceof ParameterizedType) { + return ((ParameterizedType) collectionType).getActualTypeArguments()[0]; + } + return Object.class; + } + + /** + * Returns a two element array containing this map's key and value types in positions 0 and 1 + * respectively. + */ + public static Type[] getMapKeyAndValueTypes(Type context, Class contextRawType) { + /* + * Work around a problem with the declaration of java.util.Properties. That + * class should extend Hashtable, but it's declared to + * extend Hashtable. + */ + if (Properties.class.isAssignableFrom(contextRawType)) { + return new Type[]{String.class, String.class}; + } + + Type mapType = getSupertype(context, contextRawType, Map.class); + // TODO: strip wildcards? + if (mapType instanceof ParameterizedType) { + ParameterizedType mapParameterizedType = (ParameterizedType) mapType; + return mapParameterizedType.getActualTypeArguments(); + } + return new Type[]{Object.class, Object.class}; + } + + public static Type resolve(Type context, Class contextRawType, Type toResolve) { + + return resolve(context, contextRawType, toResolve, new HashMap, Type>()); + } + + private static Type resolve( + Type context, + Class contextRawType, + Type toResolve, + Map, Type> visitedTypeVariables) { + // this implementation is made a little more complicated in an attempt to avoid object-creation + TypeVariable resolving = null; + while (true) { + if (toResolve instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) toResolve; + Type previouslyResolved = visitedTypeVariables.get(typeVariable); + if (previouslyResolved != null) { + // cannot reduce due to infinite recursion + return (previouslyResolved == Void.TYPE) ? toResolve : previouslyResolved; + } + + // Insert a placeholder to mark the fact that we are in the process of resolving this type + visitedTypeVariables.put(typeVariable, Void.TYPE); + if (resolving == null) { + resolving = typeVariable; + } + + toResolve = resolveTypeVariable(context, contextRawType, typeVariable); + if (toResolve == typeVariable) { + break; + } + + } else if (toResolve instanceof Class && ((Class) toResolve).isArray()) { + Class original = (Class) toResolve; + Type componentType = original.getComponentType(); + Type newComponentType = + resolve(context, contextRawType, componentType, visitedTypeVariables); + toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType); + break; + + } else if (toResolve instanceof GenericArrayType) { + GenericArrayType original = (GenericArrayType) toResolve; + Type componentType = original.getGenericComponentType(); + Type newComponentType = + resolve(context, contextRawType, componentType, visitedTypeVariables); + toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType); + break; + + } else if (toResolve instanceof ParameterizedType) { + ParameterizedType original = (ParameterizedType) toResolve; + Type ownerType = original.getOwnerType(); + Type newOwnerType = resolve(context, contextRawType, ownerType, visitedTypeVariables); + boolean ownerChanged = !equal(newOwnerType, ownerType); + + Type[] args = original.getActualTypeArguments(); + boolean argsChanged = false; + for (int t = 0, length = args.length; t < length; t++) { + Type resolvedTypeArgument = + resolve(context, contextRawType, args[t], visitedTypeVariables); + if (!equal(resolvedTypeArgument, args[t])) { + if (!argsChanged) { + args = args.clone(); + argsChanged = true; + } + args[t] = resolvedTypeArgument; + } + } + + toResolve = + ownerChanged || argsChanged + ? newParameterizedTypeWithOwner( + newOwnerType, (Class) original.getRawType(), args) + : original; + break; + + } else if (toResolve instanceof WildcardType) { + WildcardType original = (WildcardType) toResolve; + Type[] originalLowerBound = original.getLowerBounds(); + Type[] originalUpperBound = original.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = + resolve(context, contextRawType, originalLowerBound[0], visitedTypeVariables); + if (lowerBound != originalLowerBound[0]) { + toResolve = supertypeOf(lowerBound); + break; + } + } else if (originalUpperBound.length == 1) { + Type upperBound = + resolve(context, contextRawType, originalUpperBound[0], visitedTypeVariables); + if (upperBound != originalUpperBound[0]) { + toResolve = subtypeOf(upperBound); + break; + } + } + toResolve = original; + break; + + } else { + break; + } + } + // ensure that any in-process resolution gets updated with the final result + if (resolving != null) { + visitedTypeVariables.put(resolving, toResolve); + } + return toResolve; + } + + private static Type resolveTypeVariable( + Type context, Class contextRawType, TypeVariable unknown) { + Class declaredByRaw = declaringClassOf(unknown); + + // we can't reduce this further + if (declaredByRaw == null) { + return unknown; + } + + Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); + if (declaredBy instanceof ParameterizedType) { + int index = indexOf(declaredByRaw.getTypeParameters(), unknown); + return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; + } + + return unknown; + } + + private static int indexOf(Object[] array, Object toFind) { + for (int i = 0, length = array.length; i < length; i++) { + if (toFind.equals(array[i])) { + return i; + } + } + throw new NoSuchElementException(); + } + + /** + * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by + * a class. + */ + private static Class declaringClassOf(TypeVariable typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; + } + + static void checkNotPrimitive(Type type) { + checkArgument(!(type instanceof Class) || !((Class) type).isPrimitive()); + } + + /** + * Whether an {@linkplain ParameterizedType#getOwnerType() owner type} must be specified when + * constructing a {@link ParameterizedType} for {@code rawType}. + * + *

Note that this method might not require an owner type for all cases where Java reflection + * would create parameterized types with owner type. + */ + public static boolean requiresOwnerType(Type rawType) { + if (rawType instanceof Class) { + Class rawTypeAsClass = (Class) rawType; + return !Modifier.isStatic(rawTypeAsClass.getModifiers()) + && rawTypeAsClass.getDeclaringClass() != null; + } + return false; + } + + // Here and below we put @SuppressWarnings("serial") on fields of type `Type`. Recent Java + // compilers complain that the declared type is not Serializable. But in this context we go out of + // our way to ensure that the Type in question is either Class (which is serializable) or one of + // the nested Type implementations here (which are also serializable). + private static final class ParameterizedTypeImpl implements ParameterizedType, Serializable { + @SuppressWarnings("serial") + private final Type ownerType; + + @SuppressWarnings("serial") + private final Type rawType; + + @SuppressWarnings("serial") + private final Type[] typeArguments; + + public ParameterizedTypeImpl(Type ownerType, Class rawType, Type... typeArguments) { + requireNonNull(rawType); + + if (ownerType == null && requiresOwnerType(rawType)) { + throw new IllegalArgumentException("Must specify owner type for " + rawType); + } + + this.ownerType = ownerType == null ? null : canonicalize(ownerType); + this.rawType = canonicalize(rawType); + this.typeArguments = typeArguments.clone(); + for (int t = 0, length = this.typeArguments.length; t < length; t++) { + requireNonNull(this.typeArguments[t]); + checkNotPrimitive(this.typeArguments[t]); + this.typeArguments[t] = canonicalize(this.typeArguments[t]); + } + } + + @Override + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public boolean equals(Object other) { + return other instanceof ParameterizedType + && TypeUtils.equals(this, (ParameterizedType) other); + } + + private static int hashCodeOrZero(Object o) { + return o != null ? o.hashCode() : 0; + } + + @Override + public int hashCode() { + return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType); + } + + @Override + public String toString() { + int length = typeArguments.length; + if (length == 0) { + return typeToString(rawType); + } + + StringBuilder stringBuilder = new StringBuilder(30 * (length + 1)); + stringBuilder + .append(typeToString(rawType)) + .append("<") + .append(typeToString(typeArguments[0])); + for (int i = 1; i < length; i++) { + stringBuilder.append(", ").append(typeToString(typeArguments[i])); + } + return stringBuilder.append(">").toString(); + } + + private static final long serialVersionUID = 0; + } + + private static final class GenericArrayTypeImpl implements GenericArrayType, Serializable { + @SuppressWarnings("serial") + private final Type componentType; + + public GenericArrayTypeImpl(Type componentType) { + requireNonNull(componentType); + this.componentType = canonicalize(componentType); + } + + @Override + public Type getGenericComponentType() { + return componentType; + } + + @Override + public boolean equals(Object o) { + return o instanceof GenericArrayType && TypeUtils.equals(this, (GenericArrayType) o); + } + + @Override + public int hashCode() { + return componentType.hashCode(); + } + + @Override + public String toString() { + return typeToString(componentType) + "[]"; + } + + private static final long serialVersionUID = 0; + } + + /** + * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only + * support what the target Java version supports - at most one bound, see also + * https://bugs.openjdk.java.net/browse/JDK-8250660. If a lower bound is set, the upper bound must + * be Object.class. + */ + private static final class WildcardTypeImpl implements WildcardType, Serializable { + @SuppressWarnings("serial") + private final Type upperBound; + + @SuppressWarnings("serial") + private final Type lowerBound; + + public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + checkArgument(lowerBounds.length <= 1); + checkArgument(upperBounds.length == 1); + + if (lowerBounds.length == 1) { + requireNonNull(lowerBounds[0]); + checkNotPrimitive(lowerBounds[0]); + checkArgument(upperBounds[0] == Object.class); + this.lowerBound = canonicalize(lowerBounds[0]); + this.upperBound = Object.class; + + } else { + requireNonNull(upperBounds[0]); + checkNotPrimitive(upperBounds[0]); + this.lowerBound = null; + this.upperBound = canonicalize(upperBounds[0]); + } + } + + @Override + public Type[] getUpperBounds() { + return new Type[]{upperBound}; + } + + @Override + public Type[] getLowerBounds() { + return lowerBound != null ? new Type[]{lowerBound} : EMPTY_TYPE_ARRAY; + } + + @Override + public boolean equals(Object other) { + return other instanceof WildcardType && TypeUtils.equals(this, (WildcardType) other); + } + + @Override + public int hashCode() { + // this equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()); + return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode()); + } + + @Override + public String toString() { + if (lowerBound != null) { + return "? super " + typeToString(lowerBound); + } else if (upperBound == Object.class) { + return "?"; + } else { + return "? extends " + typeToString(upperBound); + } + } + + private static final long serialVersionUID = 0; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableField.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableField.java new file mode 100644 index 000000000..282e41c61 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableField.java @@ -0,0 +1,208 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.annotations.SerializedName; +import javafx.beans.Observable; +import javafx.beans.property.ListProperty; +import javafx.beans.property.MapProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SetProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import org.jackhuang.hmcl.util.TypeUtils; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/// @author Glavo +public abstract class ObservableField { + + public static ObservableField of(MethodHandles.Lookup lookup, Field field) { + String name; + List alternateNames; + + SerializedName serializedName = field.getAnnotation(SerializedName.class); + if (serializedName == null) { + name = field.getName(); + alternateNames = List.of(); + } else { + name = serializedName.value(); + alternateNames = List.of(serializedName.alternate()); + } + + VarHandle varHandle; + try { + varHandle = lookup.unreflectVarHandle(field); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + + if (ObservableList.class.isAssignableFrom(field.getType())) { + Type listType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), List.class); + if (!(listType instanceof ParameterizedType)) + throw new IllegalArgumentException("Cannot resolve the list type of " + field.getName()); + return new CollectionField<>(name, alternateNames, varHandle, listType); + } else if (ObservableSet.class.isAssignableFrom(field.getType())) { + Type setType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Set.class); + if (!(setType instanceof ParameterizedType)) + throw new IllegalArgumentException("Cannot resolve the set type of " + field.getName()); + + ParameterizedType listType = TypeUtils.newParameterizedTypeWithOwner( + null, + List.class, + ((ParameterizedType) setType).getActualTypeArguments()[0] + ); + return new CollectionField<>(name, alternateNames, varHandle, listType); + } else if (ObservableMap.class.isAssignableFrom(field.getType())) { + Type mapType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Map.class); + if (!(mapType instanceof ParameterizedType)) + throw new IllegalArgumentException("Cannot resolve the map type of " + field.getName()); + return new MapField<>(name, alternateNames, varHandle, mapType); + } else if (Property.class.isAssignableFrom(field.getType())) { + Type propertyType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Property.class); + if (!(propertyType instanceof ParameterizedType)) + throw new IllegalArgumentException("Cannot resolve the element type of " + field.getName()); + Type elementType = ((ParameterizedType) propertyType).getActualTypeArguments()[0]; + return new PropertyField<>(name, alternateNames, varHandle, elementType); + } else { + throw new IllegalArgumentException("Field " + field.getName() + " is not a property or observable collection"); + } + } + + protected final String serializedName; + protected final List alternateNames; + protected final VarHandle varHandle; + + private ObservableField(String serializedName, List alternateNames, VarHandle varHandle) { + this.serializedName = serializedName; + this.alternateNames = alternateNames; + this.varHandle = varHandle; + } + + public String getSerializedName() { + return serializedName; + } + + public List getAlternateNames() { + return alternateNames; + } + + public Observable get(T value) { + return (Observable) varHandle.get(value); + } + + public abstract JsonElement serialize(T value, JsonSerializationContext context); + + public abstract void deserialize(T value, JsonElement element, JsonDeserializationContext context); + + private static final class PropertyField extends ObservableField { + private final Type elementType; + + PropertyField(String serializedName, List alternate, VarHandle varHandle, Type elementType) { + super(serializedName, alternate, varHandle); + this.elementType = elementType; + } + + @Override + public JsonElement serialize(T value, JsonSerializationContext context) { + return context.serialize(((Property) get(value)).getValue()); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public void deserialize(T value, JsonElement element, JsonDeserializationContext context) { + ((Property) get(value)).setValue(context.deserialize(element, elementType)); + } + } + + private static final class CollectionField extends ObservableField { + private final Type listType; + + CollectionField(String serializedName, List alternate, VarHandle varHandle, Type listType) { + super(serializedName, alternate, varHandle); + this.listType = listType; + } + + @Override + public JsonElement serialize(T value, JsonSerializationContext context) { + return context.serialize(get(value), listType); + } + + @SuppressWarnings({"unchecked"}) + @Override + public void deserialize(T value, JsonElement element, JsonDeserializationContext context) { + List deserialized = context.deserialize(element, listType); + Object fieldValue = get(value); + + if (fieldValue instanceof ListProperty) { + ((ListProperty) fieldValue).set(FXCollections.observableList((List) deserialized)); + } else if (fieldValue instanceof ObservableList) { + ((ObservableList) fieldValue).setAll(deserialized); + } else if (fieldValue instanceof SetProperty) { + ((SetProperty) fieldValue).set(FXCollections.observableSet(new HashSet<>(deserialized))); + } else if (fieldValue instanceof ObservableSet) { + ObservableSet set = (ObservableSet) fieldValue; + set.clear(); + set.addAll(deserialized); + } else { + throw new JsonParseException("Unsupported field type: " + fieldValue.getClass()); + } + } + } + + private static final class MapField extends ObservableField { + private final Type mapType; + + MapField(String serializedName, List alternate, VarHandle varHandle, Type mapType) { + super(serializedName, alternate, varHandle); + this.mapType = mapType; + } + + @Override + public JsonElement serialize(T value, JsonSerializationContext context) { + return context.serialize(get(value), mapType); + } + + @SuppressWarnings({"unchecked"}) + @Override + public void deserialize(T config, JsonElement element, JsonDeserializationContext context) { + Map deserialized = context.deserialize(element, mapType); + ObservableMap map = (ObservableMap) varHandle.get(config); + if (map instanceof MapProperty) + ((MapProperty) map).set(FXCollections.observableMap(deserialized)); + else { + map.clear(); + map.putAll(deserialized); + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/DirtyTracker.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/DirtyTracker.java new file mode 100644 index 000000000..f7ac2500b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/DirtyTracker.java @@ -0,0 +1,71 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.javafx; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/// @author Glavo +public final class DirtyTracker { + + private final Set dirty = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Listener listener = new Listener(this); + + public void track(Observable observable) { + if (!dirty.contains(observable)) + observable.addListener(listener); + } + + public boolean isDirty(Observable observable) { + return dirty.contains(observable); + } + + public void markDirty(Observable observable) { + observable.removeListener(listener); + dirty.add(observable); + } + + private static final class Listener implements InvalidationListener, WeakListener { + + private final WeakReference trackerReference; + + public Listener(DirtyTracker trackerReference) { + this.trackerReference = new WeakReference<>(trackerReference); + } + + @Override + public boolean wasGarbageCollected() { + return trackerReference.get() == null; + } + + @Override + public void invalidated(Observable observable) { + observable.removeListener(this); + + DirtyTracker tracker = trackerReference.get(); + if (tracker != null) + tracker.markDirty(observable); + } + } +}