From bc86cd9892defb7be4d96f4ded207b485a964f03 Mon Sep 17 00:00:00 2001 From: Damon Lu <59256766+WhatDamon@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:05:00 +0800 Subject: [PATCH 1/6] update --- HMCL/build.gradle.kts | 2 +- .../java/org/jackhuang/hmcl/Launcher.java | 2 + .../java/org/jackhuang/hmcl/theme/Themes.java | 11 +- .../jackhuang/hmcl/ui/MacOSNativeUtils.java | 148 ++++++++++++++++++ .../hmcl/util/platform/NativeUtils.java | 6 +- .../hmcl/util/platform/macos/AppKit.java | 40 +++++ 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/macos/AppKit.java diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index b9fd7c0d56..2e0e511195 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -192,7 +192,7 @@ tasks.shadowJar { exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") listOf( - "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*", + "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "*-ppc", "*-ppc64le", "*-s390x", "*-armel", ).forEach { exclude("com/sun/jna/$it/**") } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index a2c583e6f6..8318bf518d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.upgrade.UpdateHandler; import org.jackhuang.hmcl.util.CrashReporter; @@ -136,6 +137,7 @@ public void start(Stage primaryStage) { // Stage.show() cannot work again because JavaFX Toolkit have already shut down. Platform.setImplicitExit(false); Controllers.initialize(primaryStage); + Themes.applyNativeDarkMode(primaryStage); UpdateChecker.init(); 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..4ba3d18600 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -36,8 +36,9 @@ import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty; import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.ui.MacOSNativeUtils; import org.jackhuang.hmcl.ui.WindowsNativeUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.NativeUtils; import org.jackhuang.hmcl.util.platform.OSVersion; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -233,6 +234,14 @@ public void handle(WindowEvent event) { } }); } + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { + MacOSNativeUtils.setAppearance(darkModeProperty().get()); + + ChangeListener listener = (observable, oldValue, newValue) -> { + MacOSNativeUtils.setAppearance(newValue); + }; + darkModeProperty().addListener(listener); + stage.getProperties().put("Themes.applyNativeDarkMode.macOS.listener", listener); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java new file mode 100644 index 0000000000..2ff9f4dc83 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -0,0 +1,148 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.ui; + +import com.sun.jna.Pointer; +import javafx.stage.Stage; +import org.jackhuang.hmcl.util.platform.macos.AppKit; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.OptionalLong; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class MacOSNativeUtils { + + private static Pointer nsApp; + private static Pointer aquaAppearance; + private static Pointer darkAquaAppearance; + private static boolean initialized; + + static { + init(); + } + + private static void init() { + if (AppKit.INSTANCE == null) { + initialized = false; + return; + } + + try { + AppKit objc = AppKit.INSTANCE; + + Pointer NSApplication = objc.objc_getClass("NSApplication"); + if (NSApplication == null) { + initialized = false; + return; + } + + Pointer sharedSel = objc.sel_registerName("sharedApplication"); + nsApp = objc.objc_msgSend(NSApplication, sharedSel); + + if (nsApp == null) { + initialized = false; + return; + } + + Pointer NSAppearance = objc.objc_getClass("NSAppearance"); + if (NSAppearance == null) { + initialized = false; + return; + } + + Pointer namedSel = objc.sel_registerName("appearanceNamed:"); + + Pointer aquaName = nsString("NSAppearanceNameAqua"); + Pointer darkAquaName = nsString("NSAppearanceNameDarkAqua"); + + if (aquaName == null || darkAquaName == null) { + initialized = false; + return; + } + + aquaAppearance = objc.objc_msgSend(NSAppearance, namedSel, aquaName); + darkAquaAppearance = objc.objc_msgSend(NSAppearance, namedSel, darkAquaName); + + initialized = (aquaAppearance != null && darkAquaAppearance != null); + } catch (Throwable t) { + LOG.warning("Failed to initialize macOS appearance support", t); + initialized = false; + } + } + + private static Pointer nsString(String value) { + if (AppKit.INSTANCE == null) return null; + + try { + AppKit objc = AppKit.INSTANCE; + Pointer NSString = objc.objc_getClass("NSString"); + Pointer sel = objc.sel_registerName("stringWithUTF8String:"); + return objc.objc_msgSend(NSString, sel, value); + } catch (Throwable t) { + LOG.warning("Failed to create NSString", t); + return null; + } + } + + public static boolean isSupported() { + return initialized; + } + + public static void setAppearance(boolean dark) { + if (!initialized || nsApp == null) return; + + try { + AppKit objc = AppKit.INSTANCE; + Pointer setSel = objc.sel_registerName("setAppearance:"); + objc.objc_msgSend(nsApp, setSel, dark ? darkAquaAppearance : aquaAppearance); + } catch (Throwable t) { + LOG.warning("Failed to set macOS appearance", t); + } + } + + public static OptionalLong getWindowHandle(Stage stage) { + try { + Class windowStageClass = Class.forName("com.sun.javafx.tk.quantum.WindowStage"); + Class glassWindowClass = Class.forName("com.sun.glass.ui.Window"); + Class tkStageClass = Class.forName("com.sun.javafx.tk.TKStage"); + + Object tkStage = MethodHandles.privateLookupIn(javafx.stage.Window.class, MethodHandles.lookup()) + .findVirtual(javafx.stage.Window.class, "getPeer", MethodType.methodType(tkStageClass)) + .invoke(stage); + + MethodHandles.Lookup windowStageLookup = MethodHandles.privateLookupIn(windowStageClass, MethodHandles.lookup()); + MethodHandle getPlatformWindow = windowStageLookup.findVirtual(windowStageClass, "getPlatformWindow", MethodType.methodType(glassWindowClass)); + Object platformWindow = getPlatformWindow.invoke(tkStage); + + long handle = (long) MethodHandles.privateLookupIn(glassWindowClass, MethodHandles.lookup()) + .findVirtual(glassWindowClass, "getNativeWindow", MethodType.methodType(long.class)) + .invoke(platformWindow); + + return OptionalLong.of(handle); + } catch (Throwable ex) { + LOG.warning("Failed to get window handle", ex); + return OptionalLong.empty(); + } + } + + private MacOSNativeUtils() { + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java index ede508155a..0dc6438637 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java @@ -61,7 +61,11 @@ private static boolean useJNA() { if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0")) return false; - // Currently we only need to use JNA on Windows + Native.getDefaultStringEncoding(); + return true; + } + + if (Platform.isMac()) { Native.getDefaultStringEncoding(); return true; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/macos/AppKit.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/macos/AppKit.java new file mode 100644 index 0000000000..1cdad26a56 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/macos/AppKit.java @@ -0,0 +1,40 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.platform.macos; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import org.jackhuang.hmcl.util.platform.NativeUtils; + +public interface AppKit extends Library { + + AppKit INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isMac() + ? Native.load("objc", AppKit.class) + : null; + + Pointer objc_getClass(String name); + + Pointer sel_registerName(String name); + + Pointer objc_msgSend(Pointer receiver, Pointer selector); + + Pointer objc_msgSend(Pointer receiver, Pointer selector, Pointer arg); + + Pointer objc_msgSend(Pointer receiver, Pointer selector, String arg); +} From 6653600bb91cf34829d25ec58819264ebf5fb876 Mon Sep 17 00:00:00 2001 From: Damon Lu <59256766+WhatDamon@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:26:07 +0800 Subject: [PATCH 2/6] update --- .../hmcl/util/platform/NativeUtils.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java index 0dc6438637..21fdbf28f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java @@ -66,6 +66,23 @@ private static boolean useJNA() { } if (Platform.isMac()) { + String osVersion = System.getProperty("os.version"); + + // Require macOS 10.14 or later + if (osVersion != null) { + String[] parts = osVersion.split("\\."); + if (parts.length >= 2) { + try { + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + if (major < 10 || (major == 10 && minor < 14)) { + return false; + } + } catch (NumberFormatException ignored) { + } + } + } + Native.getDefaultStringEncoding(); return true; } From 08248ad7ff4187d9aa8679ac6e1f1b7c800d550e Mon Sep 17 00:00:00 2001 From: Damon Lu <59256766+WhatDamon@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:34:37 +0800 Subject: [PATCH 3/6] update --- HMCL/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 2e0e511195..cd81bd328c 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -192,7 +192,7 @@ tasks.shadowJar { exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") listOf( - "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", + "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "*-ppc", "*-ppc64le", "*-s390x", "*-armel", ).forEach { exclude("com/sun/jna/$it/**") } From a1c7c1c702afc61a7f2710fb9930c3bf55db4ea5 Mon Sep 17 00:00:00 2001 From: Damon Lu <59256766+WhatDamon@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:56:18 +0800 Subject: [PATCH 4/6] update --- HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java | 7 ++----- 1 file changed, 2 insertions(+), 5 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 4ba3d18600..a26eeb9d9d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -237,11 +237,8 @@ public void handle(WindowEvent event) { } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { MacOSNativeUtils.setAppearance(darkModeProperty().get()); - ChangeListener listener = (observable, oldValue, newValue) -> { - MacOSNativeUtils.setAppearance(newValue); - }; - darkModeProperty().addListener(listener); - stage.getProperties().put("Themes.applyNativeDarkMode.macOS.listener", listener); + ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance); + stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener); } } From 8ff99a8c412305a12ca10904f7e1d62d45c3da7e Mon Sep 17 00:00:00 2001 From: Damon Lu <59256766+WhatDamon@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:42:37 +0800 Subject: [PATCH 5/6] update --- .../jackhuang/hmcl/ui/MacOSNativeUtils.java | 60 ++++++------------- 1 file changed, 18 insertions(+), 42 deletions(-) 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 2ff9f4dc83..50f8fcab3a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -31,8 +31,6 @@ public final class MacOSNativeUtils { private static Pointer nsApp; - private static Pointer aquaAppearance; - private static Pointer darkAquaAppearance; private static boolean initialized; static { @@ -57,51 +55,13 @@ private static void init() { Pointer sharedSel = objc.sel_registerName("sharedApplication"); nsApp = objc.objc_msgSend(NSApplication, sharedSel); - if (nsApp == null) { - initialized = false; - return; - } - - Pointer NSAppearance = objc.objc_getClass("NSAppearance"); - if (NSAppearance == null) { - initialized = false; - return; - } - - Pointer namedSel = objc.sel_registerName("appearanceNamed:"); - - Pointer aquaName = nsString("NSAppearanceNameAqua"); - Pointer darkAquaName = nsString("NSAppearanceNameDarkAqua"); - - if (aquaName == null || darkAquaName == null) { - initialized = false; - return; - } - - aquaAppearance = objc.objc_msgSend(NSAppearance, namedSel, aquaName); - darkAquaAppearance = objc.objc_msgSend(NSAppearance, namedSel, darkAquaName); - - initialized = (aquaAppearance != null && darkAquaAppearance != null); + initialized = (nsApp != null); } catch (Throwable t) { LOG.warning("Failed to initialize macOS appearance support", t); initialized = false; } } - private static Pointer nsString(String value) { - if (AppKit.INSTANCE == null) return null; - - try { - AppKit objc = AppKit.INSTANCE; - Pointer NSString = objc.objc_getClass("NSString"); - Pointer sel = objc.sel_registerName("stringWithUTF8String:"); - return objc.objc_msgSend(NSString, sel, value); - } catch (Throwable t) { - LOG.warning("Failed to create NSString", t); - return null; - } - } - public static boolean isSupported() { return initialized; } @@ -111,8 +71,24 @@ public static void setAppearance(boolean dark) { try { AppKit objc = AppKit.INSTANCE; + + Pointer NSAppearance = objc.objc_getClass("NSAppearance"); + if (NSAppearance == null) return; + + Pointer namedSel = objc.sel_registerName("appearanceNamed:"); + Pointer NSString = objc.objc_getClass("NSString"); + if (NSString == null) return; + + Pointer sel = objc.sel_registerName("stringWithUTF8String:"); + String appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua"; + Pointer appearanceNamePtr = objc.objc_msgSend(NSString, sel, appearanceName); + if (appearanceNamePtr == null) return; + + Pointer appearance = objc.objc_msgSend(NSAppearance, namedSel, appearanceNamePtr); + if (appearance == null) return; + Pointer setSel = objc.sel_registerName("setAppearance:"); - objc.objc_msgSend(nsApp, setSel, dark ? darkAquaAppearance : aquaAppearance); + objc.objc_msgSend(nsApp, setSel, appearance); } catch (Throwable t) { LOG.warning("Failed to set macOS appearance", t); } From 98732f07d8329af091c01a030335aad8c0eaf105 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 17 Mar 2026 21:49:07 +0800 Subject: [PATCH 6/6] Remove unused getWindowHandle() method --- .../jackhuang/hmcl/ui/MacOSNativeUtils.java | 31 ------------------- 1 file changed, 31 deletions(-) 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 50f8fcab3a..e6199e1e10 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -18,14 +18,8 @@ package org.jackhuang.hmcl.ui; import com.sun.jna.Pointer; -import javafx.stage.Stage; import org.jackhuang.hmcl.util.platform.macos.AppKit; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.OptionalLong; - import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class MacOSNativeUtils { @@ -94,31 +88,6 @@ public static void setAppearance(boolean dark) { } } - public static OptionalLong getWindowHandle(Stage stage) { - try { - Class windowStageClass = Class.forName("com.sun.javafx.tk.quantum.WindowStage"); - Class glassWindowClass = Class.forName("com.sun.glass.ui.Window"); - Class tkStageClass = Class.forName("com.sun.javafx.tk.TKStage"); - - Object tkStage = MethodHandles.privateLookupIn(javafx.stage.Window.class, MethodHandles.lookup()) - .findVirtual(javafx.stage.Window.class, "getPeer", MethodType.methodType(tkStageClass)) - .invoke(stage); - - MethodHandles.Lookup windowStageLookup = MethodHandles.privateLookupIn(windowStageClass, MethodHandles.lookup()); - MethodHandle getPlatformWindow = windowStageLookup.findVirtual(windowStageClass, "getPlatformWindow", MethodType.methodType(glassWindowClass)); - Object platformWindow = getPlatformWindow.invoke(tkStage); - - long handle = (long) MethodHandles.privateLookupIn(glassWindowClass, MethodHandles.lookup()) - .findVirtual(glassWindowClass, "getNativeWindow", MethodType.methodType(long.class)) - .invoke(platformWindow); - - return OptionalLong.of(handle); - } catch (Throwable ex) { - LOG.warning("Failed to get window handle", ex); - return OptionalLong.empty(); - } - } - private MacOSNativeUtils() { } }