From b2543deee686ebbac546ecc8935f317215d52147 Mon Sep 17 00:00:00 2001 From: glowind <121615263+glowinXD@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:19:07 +0800 Subject: [PATCH 1/2] Modify Themes.java --- .../java/org/jackhuang/hmcl/theme/Themes.java | 152 ++++++++++-------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java index fbee0b8171..c2b727d74d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -17,27 +17,24 @@ */ package org.jackhuang.hmcl.theme; -import com.sun.jna.Pointer; -import javafx.beans.Observable; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.binding.ObjectExpression; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.event.EventHandler; -import javafx.scene.paint.Color; -import javafx.stage.Stage; -import javafx.stage.WindowEvent; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + import org.glavo.monetfx.Brightness; import org.glavo.monetfx.ColorScheme; import org.glavo.monetfx.Contrast; import org.glavo.monetfx.beans.property.ColorSchemeProperty; import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty; import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.ui.WindowsNativeUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; import org.jackhuang.hmcl.util.platform.NativeUtils; import org.jackhuang.hmcl.util.platform.OSVersion; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -47,12 +44,19 @@ import org.jackhuang.hmcl.util.platform.windows.WinReg; import org.jackhuang.hmcl.util.platform.windows.WinTypes; -import java.nio.file.Path; -import java.time.Duration; -import java.util.*; +import com.sun.jna.Pointer; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.binding.ObjectExpression; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; /// @author Glavo public final class Themes { @@ -71,8 +75,9 @@ public final class Themes { private Brightness getBrightness() { String themeBrightness = config().getThemeBrightness(); - if (themeBrightness == null) + if (themeBrightness == null) { return Brightness.DEFAULT; + } return switch (themeBrightness.toLowerCase(Locale.ROOT).trim()) { case "auto" -> { @@ -82,9 +87,12 @@ private Brightness getBrightness() { yield getDefaultBrightness(); } } - case "dark" -> Brightness.DARK; - case "light" -> Brightness.LIGHT; - default -> Brightness.DEFAULT; + case "dark" -> + Brightness.DARK; + case "light" -> + Brightness.LIGHT; + default -> + Brightness.DEFAULT; }; } @@ -114,55 +122,64 @@ protected Theme computeValue() { private static Brightness defaultBrightness; private static Brightness getDefaultBrightness() { - if (defaultBrightness != null) + if (defaultBrightness != null) { return defaultBrightness; + } LOG.info("Detecting system theme brightness"); Brightness brightness = Brightness.DEFAULT; - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { - WinReg reg = WinReg.INSTANCE; - if (reg != null) { - Object appsUseLightTheme = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "AppsUseLightTheme"); - if (appsUseLightTheme instanceof Integer value) { - brightness = value == 0 ? Brightness.DARK : Brightness.LIGHT; + if (null != OperatingSystem.CURRENT_OS) { + switch (OperatingSystem.CURRENT_OS) { + case WINDOWS -> { + WinReg reg = WinReg.INSTANCE; + if (reg != null) { + Object appsUseLightTheme = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "AppsUseLightTheme"); + if (appsUseLightTheme instanceof Integer value) { + brightness = value == 0 ? Brightness.DARK : Brightness.LIGHT; + } + } } - } - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { - try { - String result = SystemUtils.run("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle").trim(); - brightness = "Dark".equalsIgnoreCase(result) ? Brightness.DARK : Brightness.LIGHT; - } catch (Exception e) { - // If the key does not exist, it means Light mode is used - brightness = Brightness.LIGHT; - } - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) { - Path dbusSend = SystemUtils.which("dbus-send"); - if (dbusSend != null) { - try { - String[] result = SystemUtils.run(List.of( - FileUtils.getAbsolutePath(dbusSend), - "--session", - "--print-reply=literal", - "--reply-timeout=1000", - "--dest=org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.Settings.Read", - "string:org.freedesktop.appearance", - "string:color-scheme" - ), Duration.ofSeconds(2)).trim().split(" "); - - if (result.length > 0) { - String value = result[result.length - 1]; - // 1: prefer dark - // 2: prefer light - if ("1".equals(value)) { - brightness = Brightness.DARK; - } else if ("2".equals(value)) { - brightness = Brightness.LIGHT; + case MACOS -> { + try { + String result = SystemUtils.run("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle").trim(); + brightness = "Dark".equalsIgnoreCase(result) ? Brightness.DARK : Brightness.LIGHT; + } catch (Exception e) { + // If the key does not exist, it means Light mode is used + brightness = Brightness.LIGHT; + } + } + case LINUX -> { + Path dbusSend = SystemUtils.which("dbus-send"); + if (dbusSend != null) { + try { + String[] result = SystemUtils.run(List.of( + FileUtils.getAbsolutePath(dbusSend), + "--session", + "--print-reply=literal", + "--reply-timeout=1000", + "--dest=org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings.Read", + "string:org.freedesktop.appearance", + "string:color-scheme" + ), Duration.ofSeconds(2)).trim().split(" "); + + if (result.length > 0) { + String value = result[result.length - 1]; + // 1: prefer dark + // 2: prefer light + if ("1".equals(value)) { + brightness = Brightness.DARK; + } else if ("2".equals(value)) { + brightness = Brightness.LIGHT; + } + } + } catch (Exception e) { + LOG.warning("Failed to get system theme from D-Bus", e); } } - } catch (Exception e) { - LOG.warning("Failed to get system theme from D-Bus", e); + } + default -> { } } } @@ -189,8 +206,8 @@ public static ColorScheme getColorScheme() { private static final ObjectBinding titleFill = Bindings.createObjectBinding( () -> config().isTitleTransparent() - ? getColorScheme().getOnSurface() - : getColorScheme().getOnPrimaryContainer(), + ? getColorScheme().getOnSurface() + : getColorScheme().getOnPrimaryContainer(), colorSchemeProperty(), config().titleTransparentProperty() ); @@ -208,8 +225,9 @@ public static void applyNativeDarkMode(Stage stage) { ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), darkMode -> { if (stage.isShowing()) { WindowsNativeUtils.getWindowHandle(stage).ifPresent(handle -> { - if (handle == WinTypes.HANDLE.INVALID_VALUE) + if (handle == WinTypes.HANDLE.INVALID_VALUE) { return; + } Dwmapi.INSTANCE.DwmSetWindowAttribute( new WinTypes.HANDLE(Pointer.createConstant(handle)), From e6dc117d366d777b4f65ac5b582f4ea8043aa5c9 Mon Sep 17 00:00:00 2001 From: glowind <121615263+glowinXD@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:48:55 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=AB=98?= =?UTF-8?q?=E5=AF=B9=E6=AF=94=E5=BA=A6=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/setting/Config.java | 89 +++++++++---- .../java/org/jackhuang/hmcl/theme/Themes.java | 47 ++++++- .../hmcl/ui/main/PersonalizationPage.java | 94 +++++++++---- .../jackhuang/hmcl/ui/main/SettingsPage.java | 125 +++++++++++------- 4 files changed, 244 insertions(+), 111 deletions(-) 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 0a845929b5..6dc14ca033 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -17,16 +17,11 @@ */ package org.jackhuang.hmcl.setting; -import com.google.gson.*; -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.annotations.SerializedName; -import javafx.beans.Observable; -import javafx.beans.property.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.collections.ObservableMap; -import javafx.collections.ObservableSet; -import javafx.scene.paint.Paint; +import java.net.Proxy; +import java.nio.file.Path; +import java.util.Map; +import java.util.TreeMap; + import org.hildan.fxgson.creators.ObservableListCreator; import org.hildan.fxgson.creators.ObservableMapCreator; import org.hildan.fxgson.creators.ObservableSetCreator; @@ -36,13 +31,39 @@ import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.theme.ThemeColor; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.util.gson.*; +import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer; +import org.jackhuang.hmcl.util.gson.ObservableSetting; +import org.jackhuang.hmcl.util.gson.PaintAdapter; +import org.jackhuang.hmcl.util.gson.PathTypeAdapter; +import org.jackhuang.hmcl.util.gson.RawPreservingObjectProperty; import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jetbrains.annotations.Nullable; -import java.net.Proxy; -import java.nio.file.Path; -import java.util.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.ToNumberPolicy; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; + +import javafx.beans.Observable; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.MapProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleMapProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.scene.paint.Paint; @JsonAdapter(value = Config.Adapter.class) public final class Config extends ObservableSetting { @@ -79,7 +100,6 @@ public String toJson() { } // Properties - @SerializedName("_version") private final IntegerProperty configVersion = new SimpleIntegerProperty(CURRENT_VERSION); @@ -96,11 +116,13 @@ public void setConfigVersion(int configVersion) { } /** - * 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. + * 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); @@ -305,7 +327,6 @@ public void setLogLines(Integer logLines) { } // UI - @SerializedName("themeBrightness") private final StringProperty themeBrightness = new SimpleStringProperty("light"); @@ -321,6 +342,21 @@ public void setThemeBrightness(String themeBrightness) { this.themeBrightness.set(themeBrightness); } + @SerializedName("themeContrast") + private final StringProperty themeContrast = new SimpleStringProperty("default"); + + public StringProperty themeContrastProperty() { + return themeContrast; + } + + public String getThemeContrast() { + return themeContrast.get(); + } + + public void setThemeContrast(String themeContrast) { + this.themeContrast.set(themeContrast); + } + @SerializedName("theme") private final ObjectProperty themeColor = new SimpleObjectProperty<>(ThemeColor.DEFAULT); @@ -383,9 +419,9 @@ public void setLauncherFontFamily(String launcherFontFamily) { @SerializedName("animationDisabled") private final BooleanProperty animationDisabled = new SimpleBooleanProperty( - FXUtils.REDUCED_MOTION == Boolean.TRUE - || !JavaRuntime.CURRENT_JIT_ENABLED - || !FXUtils.GPU_ACCELERATION_ENABLED + Boolean.TRUE.equals(FXUtils.REDUCED_MOTION) + || !JavaRuntime.CURRENT_JIT_ENABLED + || !FXUtils.GPU_ACCELERATION_ENABLED ); public BooleanProperty animationDisabledProperty() { @@ -491,7 +527,6 @@ public void setBackgroundImageOpacity(int backgroundImageOpacity) { } // Networks - @SerializedName("autoDownloadThreads") private final BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true); @@ -688,7 +723,6 @@ public void setProxyPass(String proxyPass) { } // Game - @SerializedName("disableAutoGameOptions") private final BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false); @@ -720,7 +754,6 @@ public void setAllowAutoAgent(boolean allowAutoAgent) { } // Accounts - @SerializedName("authlibInjectorServers") private final ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server}); @@ -784,7 +817,6 @@ public ObservableList> getAccountStorages() { } // Configurations - @SerializedName("last") private final StringProperty selectedProfile = new SimpleStringProperty(""); @@ -808,6 +840,7 @@ public MapProperty getConfigurations() { } public static final class Adapter extends ObservableSetting.Adapter { + @Override protected Config createInstance() { return new Config(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java index c2b727d74d..d22b066815 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import org.glavo.monetfx.Brightness; import org.glavo.monetfx.ColorScheme; @@ -67,10 +68,11 @@ public final class Themes { observables.add(config().themeBrightnessProperty()); observables.add(config().themeColorProperty()); + observables.add(config().themeContrastProperty()); if (FXUtils.DARK_MODE != null) { observables.add(FXUtils.DARK_MODE); } - bind(observables.toArray(new Observable[0])); + bind(observables.toArray(Observable[]::new)); } private Brightness getBrightness() { @@ -81,8 +83,9 @@ private Brightness getBrightness() { return switch (themeBrightness.toLowerCase(Locale.ROOT).trim()) { case "auto" -> { + boolean systemDark = Optional.ofNullable(FXUtils.DARK_MODE).map(ob -> ob.get()).orElse(false); if (FXUtils.DARK_MODE != null) { - yield FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT; + yield systemDark ? Brightness.DARK : Brightness.LIGHT; } else { yield getDefaultBrightness(); } @@ -100,7 +103,41 @@ private Brightness getBrightness() { protected Theme computeValue() { ThemeColor themeColor = Objects.requireNonNullElse(config().getThemeColor(), ThemeColor.DEFAULT); - return new Theme(themeColor, getBrightness(), Theme.DEFAULT.colorStyle(), Contrast.DEFAULT); + return new Theme(themeColor, getBrightness(), Theme.DEFAULT.colorStyle(), getContrast()); + } + + private Contrast getContrast() { + String themeContrast = config().getThemeContrast(); + if (themeContrast == null) { + return Contrast.DEFAULT; + } + + switch (themeContrast.toLowerCase(Locale.ROOT).trim()) { + case "default", "normal" -> { + return Contrast.DEFAULT; + } + case "high", "high-contrast", "hc" -> { + return getContrastByName("HIGH"); + } + default -> { + String normalized = themeContrast.toUpperCase(Locale.ROOT).trim().replace('-', '_').replace(' ', '_'); + return getContrastByName(normalized); + } + } + } + + private Contrast getContrastByName(String name) { + try { + var field = Contrast.class.getField(name); + Object val = field.get(null); + if (val instanceof Contrast c) { + return c; + } + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + + // fallback to DEFAULT + return Contrast.DEFAULT; } }; private static final ColorSchemeProperty colorScheme = new SimpleColorSchemeProperty(); @@ -222,7 +259,7 @@ public static BooleanBinding darkModeProperty() { public static void applyNativeDarkMode(Stage stage) { if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) { - ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), darkMode -> { + ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), isDark -> { if (stage.isShowing()) { WindowsNativeUtils.getWindowHandle(stage).ifPresent(handle -> { if (handle == WinTypes.HANDLE.INVALID_VALUE) { @@ -232,7 +269,7 @@ public static void applyNativeDarkMode(Stage stage) { Dwmapi.INSTANCE.DwmSetWindowAttribute( new WinTypes.HANDLE(Pointer.createConstant(handle)), WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE, - new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)), + new WinTypes.BOOLByReference(new WinTypes.BOOL(isDark)), WinTypes.BOOL.SIZE ); }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java index 6f19448095..21d2eee740 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java @@ -17,8 +17,36 @@ */ package org.jackhuang.hmcl.ui.main; -import com.jfoenix.controls.*; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig; +import org.jackhuang.hmcl.setting.EnumBackgroundImage; +import org.jackhuang.hmcl.setting.FontManager; +import org.jackhuang.hmcl.theme.ThemeColor; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.ComponentList; +import org.jackhuang.hmcl.ui.construct.ComponentSublist; +import org.jackhuang.hmcl.ui.construct.FontComboBox; +import org.jackhuang.hmcl.ui.construct.LineSelectButton; +import org.jackhuang.hmcl.ui.construct.LineToggleButton; +import org.jackhuang.hmcl.ui.construct.MultiFileItem; +import org.jackhuang.hmcl.ui.construct.URLValidator; +import org.jackhuang.hmcl.ui.construct.Validator; +import org.jackhuang.hmcl.util.Lang; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import org.jackhuang.hmcl.util.javafx.SafeStringConverter; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXColorPicker; +import com.jfoenix.controls.JFXSlider; +import com.jfoenix.controls.JFXTextField; import com.jfoenix.effects.JFXDepthManager; + import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; @@ -30,25 +58,13 @@ import javafx.scene.control.ColorPicker; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.layout.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontSmoothingType; -import org.jackhuang.hmcl.setting.EnumBackgroundImage; -import org.jackhuang.hmcl.setting.FontManager; -import org.jackhuang.hmcl.theme.ThemeColor; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.javafx.SafeStringConverter; - -import java.util.Arrays; -import java.util.Locale; -import java.util.Optional; - -import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class PersonalizationPage extends StackPane { @@ -104,6 +120,30 @@ public PersonalizationPage() { themeColorPickerContainer.getChildren().setAll(picker); Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0)); } + + { + var contrastPane = new LineSelectButton(); + contrastPane.setTitle(i18n("settings.launcher.contrast")); + contrastPane.setSubtitle(i18n("settings.take_effect_after_restart")); + + contrastPane.setItems(List.of("default", "high")); + contrastPane.setConverter(s -> { + if (s == null) { + return i18n("settings.launcher.contrast.default"); + } + return switch (s.toLowerCase(Locale.ROOT).trim()) { + case "high", "high-contrast", "hc" -> + i18n("settings.launcher.contrast.high"); + default -> + i18n("settings.launcher.contrast.default"); + }; + }); + + contrastPane.valueProperty().bindBidirectional(config().themeContrastProperty()); + + themeList.getContent().add(contrastPane); + } + { LineToggleButton titleTransparentButton = new LineToggleButton(); themeList.getContent().add(titleTransparentButton); @@ -156,7 +196,7 @@ public PersonalizationPage() { JFXSlider slider = new JFXSlider(0, 100, config().getBackgroundImageType() != EnumBackgroundImage.TRANSLUCENT - ? config().getBackgroundImageOpacity() : 50); + ? config().getBackgroundImageOpacity() : 50); slider.setShowTickMarks(true); slider.setMajorTickUnit(10); slider.setMinorTickCount(1); @@ -186,8 +226,8 @@ public void changed(ObservableValue observable, E textOpacity.textProperty().bind(valueBinding); slider.setValueFactory(s -> valueBinding); - slider.valueProperty().addListener((observable, oldValue, newValue) -> - config().setBackgroundImageOpacity(snapOpacity(newValue.doubleValue()))); + slider.valueProperty().addListener((observable, oldValue, newValue) + -> config().setBackgroundImageOpacity(snapOpacity(newValue.doubleValue()))); opacityItem.getChildren().setAll(label, slider, textOpacity); } @@ -294,10 +334,10 @@ public void changed(ObservableValue observable, E var fontAntiAliasingPane = new LineSelectButton>(); fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing")); fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart")); - fontAntiAliasingPane.setConverter(value -> - value.isPresent() - ? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT)) - : i18n("settings.launcher.font.anti_aliasing.auto") + fontAntiAliasingPane.setConverter(value + -> value.isPresent() + ? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT)) + : i18n("settings.launcher.font.anti_aliasing.auto") ); fontAntiAliasingPane.setItems( Optional.empty(), @@ -314,8 +354,8 @@ public void changed(ObservableValue observable, E fontAntiAliasingPane.setValue(Optional.empty()); } - FXUtils.onChange(fontAntiAliasingPane.valueProperty(), value -> - globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT)) + FXUtils.onChange(fontAntiAliasingPane.valueProperty(), value + -> globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT)) .orElse(null))); fontPane.getContent().add(fontAntiAliasingPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 2281016c35..c94a026178 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -17,30 +17,39 @@ */ package org.jackhuang.hmcl.ui.main; -import com.jfoenix.controls.JFXButton; -import javafx.beans.InvalidationListener; -import javafx.beans.WeakInvalidationListener; -import javafx.beans.binding.Bindings; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.StringProperty; -import javafx.css.PseudoClass; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.geometry.VPos; -import javafx.scene.Cursor; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.*; -import javafx.scene.text.TextAlignment; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + import org.jackhuang.hmcl.Metadata; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.construct.ComponentList; +import org.jackhuang.hmcl.ui.construct.ComponentSublist; +import org.jackhuang.hmcl.ui.construct.LineComponent; +import org.jackhuang.hmcl.ui.construct.LineSelectButton; +import org.jackhuang.hmcl.ui.construct.LineToggleButton; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; +import org.jackhuang.hmcl.ui.construct.MultiFileItem; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChannel; import org.jackhuang.hmcl.upgrade.UpdateChecker; @@ -49,29 +58,40 @@ import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.IOUtils; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; import org.tukaani.xz.XZInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import com.jfoenix.controls.JFXButton; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Cursor; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; public final class SettingsPage extends ScrollPane { + @SuppressWarnings("FieldCanBeLocal") private final InvalidationListener updateListener; @@ -81,7 +101,6 @@ public SettingsPage() { VBox rootPane = new VBox(); rootPane.setPadding(new Insets(10)); this.setContent(rootPane); - FXUtils.smoothScrolling(this); ComponentList settingsPane = new ComponentList(); { @@ -222,7 +241,7 @@ protected int getTrailingTextIndex() { fileCommonLocationSublist.setHasSubtitle(true); fileCommonLocationSublist.subtitleProperty().bind( Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory()) - .orElse(i18n("launcher.cache_directory.disabled")), + .orElse(i18n("launcher.cache_directory.disabled")), config().commonDirectoryProperty(), config().commonDirTypeProperty())); JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean")); @@ -239,12 +258,13 @@ protected int getTrailingTextIndex() { SupportedLocale currentLocale = I18n.getLocale(); chooseLanguagePane.setConverter(locale -> { - if (locale.isDefault()) + if (locale.isDefault()) { return locale.getDisplayName(currentLocale); - else if (locale.isSameLanguage(currentLocale)) + } else if (locale.isSameLanguage(currentLocale)) { return locale.getDisplayName(locale); - else + } else { return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale); + } }); chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales()); chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty()); @@ -279,8 +299,9 @@ else if (locale.isSameLanguage(currentLocale)) JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal")); openLogFolderButton.setOnAction(e -> openLogFolder()); openLogFolderButton.getStyleClass().add("jfx-button-border"); - if (LOG.getLogFile() == null) + if (LOG.getLogFile() == null) { openLogFolderButton.setDisable(true); + } SpinnerPane exportLogPane = new SpinnerPane(); @@ -315,6 +336,8 @@ else if (locale.isSameLanguage(currentLocale)) rootPane.getChildren().add(settingsPane); } + + Platform.runLater(() -> FXUtils.smoothScrolling(this)); } private void openLogFolder() { @@ -334,7 +357,7 @@ private static String getEntryName(Set entryNames, String name) { return name; } - for (long i = 1; ; i++) { + for (long i = 1;; i++) { String newName = name + "." + i; if (entryNames.add(newName)) { return newName; @@ -348,10 +371,10 @@ private static String getEntryName(Set entryNames, String name) { /// If an exception occurs while reading from `input`, this method returns `false`; /// If an exception occurs while writing to `output`, this method will throw it as is. private static boolean exportLogFile(ZipOutputStream output, - Path file, // For logging - String entryName, - InputStream input, - byte[] buffer) throws IOException { + Path file, // For logging + String entryName, + InputStream input, + byte[] buffer) throws IOException { //noinspection TryFinallyCanBeTryWithResources try { output.putNextEntry(new ZipEntry(entryName)); @@ -359,9 +382,10 @@ private static boolean exportLogFile(ZipOutputStream output, while (true) { try { read = input.read(buffer); - if (read <= 0) + if (read <= 0) { return true; - } catch (Throwable ex) { + } + } catch (IOException ex) { LOG.warning("Failed to decompress log file " + file, ex); return false; } @@ -371,7 +395,7 @@ private static boolean exportLogFile(ZipOutputStream output, } finally { try { input.close(); - } catch (Throwable ex) { + } catch (IOException ex) { LOG.warning("Failed to close log file " + file, ex); } output.closeEntry(); @@ -397,8 +421,7 @@ private CompletableFuture onExportLogs() { LOG.info("Exporting latest logs to " + outputFile); byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - try (var os = Files.newOutputStream(outputFile); - var zos = new ZipOutputStream(os)) { + try (var os = Files.newOutputStream(outputFile); var zos = new ZipOutputStream(os)) { Set entryNames = new HashSet<>(); @@ -417,25 +440,25 @@ private CompletableFuture onExportLogs() { input = "gz".equals(extension) ? new GZIPInputStream(input) : new XZInputStream(input); - } catch (Throwable ex) { + } catch (IOException ex) { LOG.warning("Failed to open log file " + path, ex); IOUtils.closeQuietly(input, ex); input = null; } String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, ".")); - if (input != null && exportLogFile(zos, path, entryName, input, buffer)) + if (input != null && exportLogFile(zos, path, entryName, input, buffer)) { continue; + } } // Copy the log file content as-is into a new entry in the zip file. // If an exception occurs while decompressing the input file, we should // ensure the input file and the current zip entry are closed. - InputStream input; try { input = Files.newInputStream(path); - } catch (Throwable ex) { + } catch (IOException ex) { LOG.warning("Failed to open log file " + path, ex); continue; }