diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 2737b3afd..00137f2df 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -128,11 +128,13 @@ val addOpens = listOf( "javafx.base/javafx.beans.property", "javafx.graphics/javafx.css", "javafx.graphics/javafx.stage", + "javafx.graphics/com.sun.glass.ui", "javafx.graphics/com.sun.javafx.stage", "javafx.graphics/com.sun.javafx.util", "javafx.graphics/com.sun.prism", "javafx.controls/com.sun.javafx.scene.control", "javafx.controls/com.sun.javafx.scene.control.behavior", + "javafx.graphics/com.sun.javafx.tk.quantum", "javafx.controls/javafx.scene.control.skin", "jdk.attach/sun.tools.attach", ) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 785c4f800..b8eb556d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXListView; +import com.sun.jna.Pointer; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; @@ -28,6 +29,7 @@ import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.PseudoClass; +import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; @@ -37,17 +39,21 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; import javafx.scene.layout.*; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import org.jackhuang.hmcl.game.GameDumpGenerator; import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.setting.StyleSheets; +import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.CircularArrayList; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; -import org.jackhuang.hmcl.util.platform.ManagedProcess; -import org.jackhuang.hmcl.util.platform.SystemUtils; +import org.jackhuang.hmcl.util.platform.*; +import org.jackhuang.hmcl.util.platform.windows.Dwmapi; +import org.jackhuang.hmcl.util.platform.windows.WinConstants; +import org.jackhuang.hmcl.util.platform.windows.WinTypes; import java.io.IOException; import java.nio.file.Files; @@ -84,6 +90,36 @@ public final class LogWindow extends Stage { private final LogWindowImpl impl; private final ManagedProcess gameProcess; + @SuppressWarnings("unused") + private Object windowsDarkModeListenerHolder; + + { + if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) { + this.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() { + @Override + public void handle(WindowEvent event) { + LogWindow.this.removeEventFilter(WindowEvent.WINDOW_SHOWN, this); + + windowsDarkModeListenerHolder = FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> { + if (LogWindow.this.isShowing()) { + WindowsNativeUtils.getWindowHandle(LogWindow.this).ifPresent(handle -> { + if (handle == WinTypes.HANDLE.INVALID_VALUE) + return; + + Dwmapi.INSTANCE.DwmSetWindowAttribute( + new WinTypes.HANDLE(Pointer.createConstant(handle)), + WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE, + new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)), + WinTypes.BOOL.SIZE + ); + }); + } + }); + } + }); + } + } + public LogWindow(ManagedProcess gameProcess) { this(gameProcess, new CircularArrayList<>()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WindowsNativeUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WindowsNativeUtils.java new file mode 100644 index 000000000..3de245ec5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WindowsNativeUtils.java @@ -0,0 +1,60 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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 javafx.stage.Stage; +import javafx.stage.Window; + +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; + +/// @author Glavo +public final class WindowsNativeUtils { + + 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(Window.class, MethodHandles.lookup()) + .findVirtual(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 WindowsNativeUtils() { + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Dwmapi.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Dwmapi.java new file mode 100644 index 000000000..6d5de237f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Dwmapi.java @@ -0,0 +1,32 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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.windows; + +import com.sun.jna.PointerType; +import com.sun.jna.win32.StdCallLibrary; +import org.jackhuang.hmcl.util.platform.NativeUtils; + +/// @author Glavo +public interface Dwmapi extends StdCallLibrary { + Dwmapi INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows() + ? NativeUtils.load("dwmapi", Dwmapi.class) + : null; + + /// @see DwmSetWindowAttribute function + int DwmSetWindowAttribute(WinTypes.HANDLE hwnd, int dwAttribute, PointerType pvAttribute, int cbAttribute); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java index e76a6c38d..01888d030 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java @@ -96,4 +96,8 @@ public interface WinConstants { int RelationNumaNodeEx = 6; int RelationProcessorModule = 7; int RelationAll = 0xffff; + + // https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute + int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java index 1f639842a..8c0e43ae4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.util.platform.windows; import com.sun.jna.*; +import com.sun.jna.ptr.ByReference; import com.sun.jna.ptr.LongByReference; import java.util.Arrays; @@ -27,6 +28,97 @@ import java.util.List; * @author Glavo */ public interface WinTypes { + + /// @see Windows Data Types + final class BOOL extends IntegerType { + + public static final int SIZE = 4; + + public BOOL() { + this(0); + } + + public BOOL(boolean value) { + this(value ? 1L : 0L); + } + + public BOOL(long value) { + super(SIZE, value, false); + assert value == 0 || value == 1; + } + + public boolean booleanValue() { + return this.intValue() > 0; + } + + @Override + public String toString() { + return Boolean.toString(booleanValue()); + } + + } + + /// @see Windows Data Types + final class BOOLByReference extends ByReference { + + public BOOLByReference() { + this(new BOOL(0)); + } + + public BOOLByReference(BOOL value) { + super(BOOL.SIZE); + setValue(value); + } + + public void setValue(BOOL value) { + getPointer().setInt(0, value.intValue()); + } + + public BOOL getValue() { + return new BOOL(getPointer().getInt(0)); + } + } + + /// @see Windows Data Types + final class HANDLE extends PointerType { + public static final long INVALID_VALUE = Native.POINTER_SIZE == 8 ? -1 : 0xFFFFFFFFL; + + public static final HANDLE INVALID = new HANDLE(Pointer.createConstant(INVALID_VALUE)); + + private boolean immutable; + + public HANDLE() { + } + + public HANDLE(Pointer p) { + setPointer(p); + immutable = true; + } + + @Override + public Object fromNative(Object nativeValue, FromNativeContext context) { + Object o = super.fromNative(nativeValue, context); + if (INVALID.equals(o)) { + return INVALID; + } + return o; + } + + @Override + public void setPointer(Pointer p) { + if (immutable) { + throw new UnsupportedOperationException("immutable reference"); + } + + super.setPointer(p); + } + + @Override + public String toString() { + return String.valueOf(getPointer()); + } + } + /** * @see OSVERSIONINFOEXW structure */