Windows 11 上日志窗口标题栏颜色应当跟随主题模式设置 (#4910)

This commit is contained in:
Glavo
2025-12-04 01:24:36 +08:00
committed by GitHub
parent ed4fec6c42
commit 7ac22ad567
6 changed files with 228 additions and 2 deletions

View File

@@ -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",
)

View File

@@ -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<>());
}

View File

@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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() {
}
}

View File

@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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 <a href="https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmsetwindowattribute">DwmSetWindowAttribute function</a>
int DwmSetWindowAttribute(WinTypes.HANDLE hwnd, int dwAttribute, PointerType pvAttribute, int cbAttribute);
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
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 <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
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 <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
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 <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-osversioninfoexw">OSVERSIONINFOEXW structure</a>
*/