Skip to content

Commit f5768f2

Browse files
WhatDamonGlavo
andauthored
在 macOS 下随明暗模式设置 NSAppearance (#5778)
Co-authored-by: Glavo <zjx001202@gmail.com>
1 parent 2d377d0 commit f5768f2

6 files changed

Lines changed: 181 additions & 3 deletions

File tree

HMCL/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ tasks.shadowJar {
195195
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
196196

197197
listOf(
198-
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
198+
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
199199
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
200200
).forEach { exclude("com/sun/jna/$it/**") }
201201

HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.jackhuang.hmcl.task.Schedulers;
4040
import org.jackhuang.hmcl.ui.Controllers;
4141
import org.jackhuang.hmcl.ui.FXUtils;
42+
import org.jackhuang.hmcl.theme.Themes;
4243
import org.jackhuang.hmcl.upgrade.UpdateChecker;
4344
import org.jackhuang.hmcl.upgrade.UpdateHandler;
4445
import org.jackhuang.hmcl.util.CrashReporter;
@@ -138,6 +139,9 @@ public void start(Stage primaryStage) {
138139
Platform.setImplicitExit(false);
139140
Controllers.initialize(primaryStage);
140141

142+
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
143+
Themes.applyNativeDarkMode(primaryStage);
144+
141145
UpdateChecker.init();
142146

143147
primaryStage.show();

HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
3737
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
3838
import org.jackhuang.hmcl.ui.FXUtils;
39-
import org.jackhuang.hmcl.util.io.FileUtils;
39+
import org.jackhuang.hmcl.ui.MacOSNativeUtils;
4040
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
41+
import org.jackhuang.hmcl.util.io.FileUtils;
4142
import org.jackhuang.hmcl.util.platform.NativeUtils;
4243
import org.jackhuang.hmcl.util.platform.OSVersion;
4344
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -233,6 +234,11 @@ public void handle(WindowEvent event) {
233234
}
234235
});
235236
}
237+
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) {
238+
MacOSNativeUtils.setAppearance(darkModeProperty().get());
239+
240+
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
241+
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
236242
}
237243
}
238244

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.ui;
19+
20+
import com.sun.jna.Pointer;
21+
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
22+
import org.jetbrains.annotations.Nullable;
23+
24+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
25+
26+
public final class MacOSNativeUtils {
27+
28+
private static final Pointer nsApp = init();
29+
30+
private static @Nullable Pointer init() {
31+
if (ObjectiveCRuntime.INSTANCE == null) {
32+
return null;
33+
}
34+
35+
try {
36+
var objc = ObjectiveCRuntime.INSTANCE;
37+
38+
Pointer nsApplication = objc.objc_getClass("NSApplication");
39+
if (!isNull(nsApplication)) {
40+
Pointer sharedSel = objc.sel_registerName("sharedApplication");
41+
if (!isNull(sharedSel))
42+
return objc.objc_msgSend(nsApplication, sharedSel);
43+
}
44+
} catch (Throwable e) {
45+
LOG.warning("Failed to initialize macOS appearance support", e);
46+
}
47+
48+
return null;
49+
}
50+
51+
public static boolean isSupported() {
52+
return nsApp != null;
53+
}
54+
55+
private static boolean isNull(Pointer pointer) {
56+
return pointer == null || Pointer.nativeValue(pointer) == 0;
57+
}
58+
59+
public static void setAppearance(boolean dark) {
60+
setAppearance(dark, false);
61+
}
62+
63+
public static void setAppearance(boolean dark, boolean highContrast) {
64+
if (nsApp == null) return;
65+
66+
try {
67+
var objc = ObjectiveCRuntime.INSTANCE;
68+
69+
Pointer nsAppearance = objc.objc_getClass("NSAppearance");
70+
if (isNull(nsAppearance))
71+
return;
72+
73+
Pointer namedSel = objc.sel_registerName("appearanceNamed:");
74+
Pointer nsString = objc.objc_getClass("NSString");
75+
if (isNull(nsString)) return;
76+
77+
Pointer sel = objc.sel_registerName("stringWithUTF8String:");
78+
79+
String appearanceName;
80+
if (highContrast) {
81+
appearanceName = dark ? "NSAppearanceNameAccessibilityHighContrastDarkAqua" : "NSAppearanceNameAccessibilityHighContrastAqua";
82+
} else {
83+
appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua";
84+
}
85+
86+
Pointer appearanceNamePtr = objc.objc_msgSend(nsString, sel, appearanceName);
87+
if (isNull(appearanceNamePtr)) return;
88+
89+
Pointer appearance = objc.objc_msgSend(nsAppearance, namedSel, appearanceNamePtr);
90+
if (isNull(appearance)) return;
91+
92+
Pointer setSel = objc.sel_registerName("setAppearance:");
93+
objc.objc_msgSend(nsApp, setSel, appearance);
94+
} catch (Throwable t) {
95+
LOG.warning("Failed to set macOS appearance", t);
96+
}
97+
}
98+
99+
private MacOSNativeUtils() {
100+
}
101+
}

HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,28 @@ private static boolean useJNA() {
6161
if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0"))
6262
return false;
6363

64-
// Currently we only need to use JNA on Windows
64+
Native.getDefaultStringEncoding();
65+
return true;
66+
}
67+
68+
if (Platform.isMac()) {
69+
String osVersion = System.getProperty("os.version");
70+
71+
// Require macOS 10.14 or later
72+
if (osVersion != null) {
73+
String[] parts = osVersion.split("\\.");
74+
if (parts.length >= 2) {
75+
try {
76+
int major = Integer.parseInt(parts[0]);
77+
int minor = Integer.parseInt(parts[1]);
78+
if (major < 10 || (major == 10 && minor < 14)) {
79+
return false;
80+
}
81+
} catch (NumberFormatException ignored) {
82+
}
83+
}
84+
}
85+
6586
Native.getDefaultStringEncoding();
6687
return true;
6788
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util.platform.macos;
19+
20+
import com.sun.jna.Library;
21+
import com.sun.jna.Pointer;
22+
import org.jackhuang.hmcl.util.platform.NativeUtils;
23+
24+
/// @see <a href="https://developer.apple.com/documentation/objectivec/objective-c-runtime">Objective-C Runtime</a>
25+
public interface ObjectiveCRuntime extends Library {
26+
27+
/// The Objective-C runtime library instance.
28+
ObjectiveCRuntime INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isMac()
29+
? NativeUtils.load("objc", ObjectiveCRuntime.class)
30+
: null;
31+
32+
/// @see <a href="https://developer.apple.com/documentation/objectivec/objc_getclass(_:)">objc_getClass function</a>
33+
Pointer objc_getClass(String name);
34+
35+
/// @see <a href="https://developer.apple.com/documentation/objectivec/sel_registername(_:)">sel_registerName function</a>
36+
Pointer sel_registerName(String name);
37+
38+
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
39+
Pointer objc_msgSend(Pointer receiver, Pointer selector);
40+
41+
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
42+
Pointer objc_msgSend(Pointer receiver, Pointer selector, Pointer arg);
43+
44+
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
45+
Pointer objc_msgSend(Pointer receiver, Pointer selector, String arg);
46+
}

0 commit comments

Comments
 (0)