diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index b9fd7c0d56..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-*", "linux-*", "darwin-*", + "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "*-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..a26eeb9d9d 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,11 @@ public void handle(WindowEvent event) { } }); } + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { + MacOSNativeUtils.setAppearance(darkModeProperty().get()); + + ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance); + stage.getProperties().put("Themes.applyNativeDarkMode.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..e6199e1e10 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -0,0 +1,93 @@ +/* + * 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 org.jackhuang.hmcl.util.platform.macos.AppKit; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class MacOSNativeUtils { + + private static Pointer nsApp; + 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); + + initialized = (nsApp != null); + } catch (Throwable t) { + LOG.warning("Failed to initialize macOS appearance support", t); + initialized = false; + } + } + + public static boolean isSupported() { + return initialized; + } + + public static void setAppearance(boolean dark) { + if (!initialized || nsApp == null) return; + + 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, appearance); + } catch (Throwable t) { + LOG.warning("Failed to set macOS appearance", t); + } + } + + 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..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 @@ -61,7 +61,28 @@ 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()) { + 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; } 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); +}