From 0a25f3fd026f970a29d76d1a0f09b3d275194923 Mon Sep 17 00:00:00 2001 From: dwgx <143298346+dwgx@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:23:24 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=E4=BF=AE=E5=A4=8D=20macOS=20=E4=B8=8B?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E6=A8=A1=E5=BC=8F=E3=80=8C=E8=B7=9F=E9=9A=8F?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE=E3=80=8D=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5778 之后,HMCL 在 macOS 上会调用 NSApplication setAppearance: 把应用外观 固定为启动器自己的明暗模式,用来让文件选择器等原生窗口跟随启动器配色。 但 JavaFX 的 Platform.Preferences.colorScheme 正是由 NSApp.effectiveAppearance 推导出来的(PlatformSupport.m 对 effectiveAppearance 注册 KVO,再用它解析 NSColor,PreferenceProperties 比较前景色与背景色亮度得出明暗)。setAppearance: 改的就是 effectiveAppearance,因此 colorScheme 会变成启动器自己写入的值, 不再反映系统设置,形成自我反馈: FXUtils.DARK_MODE ← colorScheme ← effectiveAppearance ← 启动器写入的值 于是「跟随系统设置」读到的是启动器自己的明暗模式,切换该选项没有效果, 系统明暗模式变化后启动器也不再跟随。 修复分两处: - 在 macOS 上改为直接读取 AppleInterfaceStyle 用户默认项获取系统明暗模式。 该值反映系统设置,不受 setAppearance: 影响。FXUtils.DARK_MODE 仍作为 失效触发器保留,只是不再作为取值来源,因此重算会收敛。 - 仅在启动器明暗模式与系统不一致时固定应用外观;一致时清除外观,让应用 重新继承系统外观,JavaFX 得以继续观察系统变化。由于主题包可以通过 resolveCurrentThemeBrightness 覆盖明暗模式,这里比较的是已解析的 darkModeProperty 而非明暗模式设置项,以免主题包指定的外观被错误清除。 用户显式选择 light/dark 时外观仍会被固定,文件选择器配色行为不变。 改动仅在 macOS 分支内,Windows 与 Linux 的明暗模式检测路径未受影响。 Closes #6526 --- .../java/org/jackhuang/hmcl/theme/Themes.java | 33 ++++++++++- .../jackhuang/hmcl/ui/MacOSNativeUtils.java | 57 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 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 b0bb6a9a772..17c0caeceb9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -466,6 +466,16 @@ public static Brightness getCurrentBrightness() { /// Returns the brightness requested by the current system or platform settings. private static Brightness getAutomaticBrightness() { + // On macOS the launcher pins the application appearance (see #applyNativeDarkMode), and JavaFX derives its + // color scheme from NSApp.effectiveAppearance. Reading FXUtils.DARK_MODE here would therefore read back the + // brightness this launcher itself applied, so the system setting is read directly instead. + if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { + @Nullable Boolean systemDarkMode = MacOSNativeUtils.isSystemInDarkMode(); + if (systemDarkMode != null) { + return systemDarkMode ? Brightness.DARK : Brightness.LIGHT; + } + } + if (FXUtils.DARK_MODE != null) { return FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT; } @@ -1151,13 +1161,32 @@ public void handle(WindowEvent event) { }); } } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { - MacOSNativeUtils.setAppearance(darkModeProperty().get()); + applyMacOSAppearance(darkModeProperty().get()); - ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance); + ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), Themes::applyMacOSAppearance); stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener); } } + /// Applies the launcher brightness to the macOS application appearance. + /// + /// Setting an explicit appearance is what makes native windows owned by the application - notably the file + /// chooser - follow the launcher brightness instead of the system one. It has a cost: while an appearance is + /// pinned, `NSApp.effectiveAppearance` no longer reports the system appearance, so JavaFX stops reporting + /// system color scheme changes. + /// + /// The appearance is therefore only pinned while the launcher brightness actually differs from the system + /// brightness. When they agree - which is the case whenever the brightness mode follows the system - the + /// appearance is cleared, so the system appearance is inherited and JavaFX keeps observing system changes. + private static void applyMacOSAppearance(boolean dark) { + @Nullable Boolean systemDarkMode = MacOSNativeUtils.isSystemInDarkMode(); + if (systemDarkMode != null && systemDarkMode == dark) { + MacOSNativeUtils.clearAppearance(); + } else { + MacOSNativeUtils.setAppearance(dark); + } + } + /// Prevents instantiation. private Themes() { } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java index 9d83ce300fd..80b6d6f70a9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -96,6 +96,63 @@ public static void setAppearance(boolean dark, boolean highContrast) { } } + /// Clears the explicit application appearance, so that the application inherits the system appearance again. + /// + /// While an explicit appearance is set, `NSApp.effectiveAppearance` reports that appearance rather than the + /// system one. JavaFX derives [javafx.application.Platform.Preferences#colorSchemeProperty()] from + /// `effectiveAppearance`, so leaving an appearance pinned makes the reported color scheme follow the launcher + /// instead of the system. + public static void clearAppearance() { + if (nsApp == null) return; + + try { + var objc = ObjectiveCRuntime.INSTANCE; + // A null Pointer maps to Objective-C nil, which resets the appearance to inherited. + objc.objc_msgSend(nsApp, objc.sel_registerName("setAppearance:"), (Pointer) null); + } catch (Throwable t) { + LOG.warning("Failed to clear macOS appearance", t); + } + } + + /// Reads whether the system is currently using dark mode, independently of the application appearance. + /// + /// This reads the `AppleInterfaceStyle` user default, which reflects the system setting and is unaffected by + /// [#setAppearance(boolean)]. It must not be derived from `effectiveAppearance` or from the JavaFX color + /// scheme, because both report the pinned application appearance once one has been set. + /// + /// @return whether the system is in dark mode, or `null` if it could not be determined + public static @Nullable Boolean isSystemInDarkMode() { + if (nsApp == null) return null; + + try { + var objc = ObjectiveCRuntime.INSTANCE; + + @Nullable Pointer userDefaults = objc.objc_getClass("NSUserDefaults"); + if (isNull(userDefaults)) return null; + + @Nullable Pointer defaults = objc.objc_msgSend(userDefaults, objc.sel_registerName("standardUserDefaults")); + if (isNull(defaults)) return null; + + @Nullable Pointer nsString = objc.objc_getClass("NSString"); + if (isNull(nsString)) return null; + + @Nullable Pointer key = objc.objc_msgSend(nsString, objc.sel_registerName("stringWithUTF8String:"), "AppleInterfaceStyle"); + if (isNull(key)) return null; + + @Nullable Pointer value = objc.objc_msgSend(defaults, objc.sel_registerName("stringForKey:"), key); + // The key is absent in light mode. + if (isNull(value)) return Boolean.FALSE; + + @Nullable Pointer utf8 = objc.objc_msgSend(value, objc.sel_registerName("UTF8String")); + if (isNull(utf8)) return null; + + return "Dark".equalsIgnoreCase(utf8.getString(0)); + } catch (Throwable t) { + LOG.warning("Failed to read macOS system appearance", t); + return null; + } + } + private MacOSNativeUtils() { } }