Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
40 changes: 38 additions & 2 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
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;
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;
Expand All @@ -37,17 +39,21 @@
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;
Expand Down Expand Up @@ -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<>());
}
Expand Down
60 changes: 60 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/WindowsNativeUtils.java
Original file line number Diff line number Diff line change
@@ -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() {
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,97 @@
* @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>
*/
Expand Down