Compare commits

...
34 Commits
Author SHA1 Message Date
辞庐 2dccd8099d Bump authlib-injector to 1.2.8 (#6390) 2026-07-13 20:33:23 +08:00
Glavo d8a03cfed8 支持主题色跟随系统 (#6385) 2026-07-13 03:42:50 +08:00
Glavo 26a874a030 Bump Gradle to 9.6.1 (#6388) 2026-07-13 03:00:15 +08:00
Glavo 7c5c3e2fb4 使用 Gradle Wrapper Neo 代替 Gradle Wrapper (#6387) 2026-07-13 02:52:47 +08:00
Glavo 62d7b36248 添加“窗口透明”选项 (#6383) 2026-07-13 00:41:12 +08:00
Glavo dec6837528 修复 Minecraft 26.1+ 无法安装 Quilt 的问题 (#6386) 2026-07-13 00:38:52 +08:00
快乐凤爪王 3484792e81 Feat: Added status label opacity when installer item is incompatible (#6335) 2026-07-12 22:37:57 +08:00
快乐凤爪王 2b310e035d Fix: Lock AccountListItemSkin.btnRefresh width to prevent layout shifting (#6369) 2026-07-12 22:01:34 +08:00
Glavo 6e960612dd 固定默认游戏文件夹和官方启动器游戏文件夹的 ID (#6382) 2026-07-12 22:00:30 +08:00
辞庐 73a776b021 fix: 整合包安装界面高度限制失效 (#6371) 2026-07-12 21:46:46 +08:00
辞庐and3gf8jv4dv 31a2f49ed2 更新 fatal.unsupported_platform 设置指引 (#6251)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-07-12 20:49:51 +08:00
快乐凤爪王 ab268cfe48 Feat: DisplayName for JavaPackageType (#6377) 2026-07-12 20:35:32 +08:00
Glavo 721aeccfc0 修复 JFXPasswordField 导致 CPU 占用率异常增高的问题 (#6373) 2026-07-12 01:35:01 +08:00
Glavo e4348f727a 优化下载线程数设置 UI (#6374) 2026-07-12 01:34:17 +08:00
快乐凤爪王 dfc96f10da Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin (#6370) 2026-07-11 21:19:37 +08:00
快乐凤爪王 a5c7fe08d8 Feat: No results found tip for ModDownloadListPage (#6368) 2026-07-11 02:37:09 +08:00
Glavo a6e0acca45 在滚动条处于按下状态时保持不透明状态 (#6366) 2026-07-10 23:57:20 +08:00
Glavo 3d312f9247 修复在 JFXComboBox 弹出窗口的滚动条上释放鼠标时也会导致弹出窗口隐藏的问题 (#6365) 2026-07-10 21:44:51 +08:00
Glavo 18dc5b69c0 Fix #6320: 修复安装新实例时,Fabric API 安装路径未遵循版本隔离策略的问题 (#6355) 2026-07-10 20:36:29 +08:00
快乐凤爪王 98af2c85b6 Feat: Animate menu button arrow counter-clockwise on popup display (#6344) 2026-07-10 20:35:39 +08:00
Glavo c4c0b1a1ce 修复自动分配内存逻辑异常的问题 (#6356) 2026-07-10 20:21:44 +08:00
辞庐 5e621ce340 HttpRequest 添加 PUT 与 DELETE 请求支持 (#6312) 2026-07-09 21:02:14 +08:00
快乐凤爪王 1d1c8dfdc9 Fix: Adjusted the size of the ModInfoListCell ResourcePackListCell icon (#6347) 2026-07-09 20:55:33 +08:00
快乐凤爪王 9b68b8825d Chore: Removed unnecessary punctuation marks of allow_auto_agent.subtitle (#6346) 2026-07-09 20:55:11 +08:00
快乐凤爪王 5c17783223 Fix: Restore subtitle when RemoteVersionListCell is reused (#6329) 2026-07-08 22:03:59 +08:00
Glavo a921be45f1 Fix #6290: GameAdvancedListItem 应等待 HMCLGameRepository 加载完成后再加载图标 (#6325) 2026-07-08 21:59:06 +08:00
辞庐 2dd1c0f85e 优化日志窗口配色 (#6313) 2026-07-08 21:54:24 +08:00
快乐凤爪王 7ba229b3e5 Fix: Adjust GameListCell spacing (#6337) 2026-07-08 21:47:49 +08:00
辞庐 59a299630b fix: #6328 MCIM 描述没有对齐 (#6333) 2026-07-08 21:46:26 +08:00
快乐凤爪王 b41b89e233 Feat: Added numbered square tag to Java runtime list cells (#6334) 2026-07-08 21:40:50 +08:00
ToobLac af137e5ff1 [Bugfix] 修复 #6338: macOS 导出整合包默认名称包含2个后缀名 (#6340)
Fixes #6338
2026-07-08 21:39:37 +08:00
Glavo 74ddbf5623 Streamline memory allocation options and improve preset management (#6330)
Fix #6307
2026-07-08 21:15:36 +08:00
快乐凤爪王 accef4a93f Fix: WorldListPage MDListCell tags (#6339) 2026-07-08 20:37:16 +08:00
快乐凤爪王 9c3a955fad Refactor: Launch pane button (#6341) 2026-07-08 20:27:35 +08:00
76 changed files with 6105 additions and 708 deletions

No files matched your search

@@ -38,7 +38,6 @@ import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
@@ -68,14 +67,18 @@ public class JFXPasswordField extends PasswordField {
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
}
useJFXContextMenu(this);
}
/// Prevents the legacy JFoenix skin from continuously requesting another layout pass.
// https://github.com/HMCL-dev/HMCL/issues/5822
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
@Override
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
/**
* Initialize the style class to 'jfx-password-field'.
* <p>
@@ -294,4 +297,3 @@ public class JFXPasswordField extends PasswordField {
}
}
@@ -15,9 +15,13 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@@ -25,6 +29,8 @@ import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import java.util.List;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
@@ -150,6 +156,30 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
}
});
for (Node child : getChildren()) {
if (child instanceof ListView<?> listView && "list-view".equals(listView.getId())) {
// The default hideOnClick logic of ComboBoxListViewSkin doesn't work well with FloatScrollBarSkin.
// We implement hideOnClick ourselves to prevent the popup from
// hiding when releasing the mouse on the scrollbar.
this.setHideOnClick(false);
listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
EventTarget target = t.getTarget();
if (target instanceof Node targetNode) {
List<String> s = targetNode.getStyleClass();
if (s.contains("thumb")
|| s.contains("track")
|| s.contains("decrement-arrow")
|| s.contains("increment-arrow")) {
return;
}
}
comboBox.hide();
});
}
}
}
protected void layoutChildren(double x, double y, double w, double h) {
@@ -242,6 +242,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id));
}
/// Removes a version from disk and drops any cached instance settings for that version.
@Override
public boolean removeVersionFromDisk(String id) {
boolean removed = super.removeVersionFromDisk(id);
if (removed) {
instanceGameSettings.remove(id);
loadedInstanceGameSettings.remove(id);
readOnlyInstanceGameSettings.remove(id);
beingModpackVersions.remove(id);
}
return removed;
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
@@ -382,8 +395,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
+ file + ", Actual: " + schemaResult.actual());
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA ->
LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
}
}
@@ -520,31 +534,35 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
public boolean shouldIsolateNewInstance(boolean modded) {
GameSettings.Preset preset = getParentGameSettings(null);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
return switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> modded;
};
}
public void applyDefaultIsolationSetting(String id) {
if (!hasVersion(id)) {
/// Applies default isolation to a new instance before the version metadata is saved.
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
return;
}
GameSettings.Instance instanceSetting = getInstanceGameSettings(id);
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
boolean isolated = switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
};
if (isolated) {
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
if (setting != null) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
GameSettings.Instance setting = getInstanceGameSettings(id);
if (setting == null) {
setting = initInstanceGameSettings(id, new GameSettings.Instance());
}
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
saveGameSettings(id);
}
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
}
public Optional<Path> getVersionIconFile(String id) {
@@ -684,7 +702,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
/// Result of loading an instance-specific game settings file.
///
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param allowSave whether the file may be overwritten
private record InstanceGameSettingsLoadResult(
@Nullable GameSettings.Instance setting,
@@ -694,26 +712,31 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
GameSettings.Effective vs = getEffectiveGameSettings(version);
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
boolean autoMemory = vs.get(GameSettings::autoMemoryProperty);
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
@Nullable Integer maxMemory;
if (autoMemory) {
maxMemory = noJVMOptions
? null
: Math.toIntExact(getAutoAllocatedMemory(SystemInfo.getPhysicalMemoryStatus().available()) / 1024L / 1024L);
} else {
maxMemory = vs.getMaxMemory();
}
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.get(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty)))
.setMaxMemory(noJVMOptions && autoMemory ? null : (int) (getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
SystemInfo.getPhysicalMemoryStatus().available(),
autoMemory
) / 1024 / 1024))
.setMinMemory(vs.get(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.get(GameSettings::permSizeProperty)))
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
.setMaxMemory(maxMemory)
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.get(GameSettings::environmentVariablesProperty))
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
.stream()
.map(it -> {
int idx = it.indexOf('=');
@@ -731,16 +754,16 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
.setNoGeneratedJVMArgs(noJVMOptions)
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
.setUseCustomNatives(vs.get(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.get(GameSettings::nativesDirectoryProperty))
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
.setRenderer(vs.getRenderer(gameVersionNumber))
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
.setUseNativeGLFW(vs.get(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.get(GameSettings::useNativeOpenALProperty))
.setUseNativeGLFW(vs.getInheritable(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
@@ -838,22 +861,21 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
public static long getAutoAllocatedMemory(long available) {
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (usable <= 0) {
return available;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested;
if (usable <= threshold)
suggested = (long) (usable * 0.8);
else
suggested = Math.min(
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return suggested;
}
public static ProxyOption getProxyOption() {
@@ -544,7 +544,7 @@ public final class LauncherHelper {
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (!setting.get(GameSettings::useCustomNativesProperty)) {
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
@@ -581,7 +581,7 @@ public final class LauncherHelper {
}
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
if (java.getBits() == Bits.BIT_32 && !setting.getInheritable(GameSettings::autoMemoryProperty) && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
@@ -628,7 +628,7 @@ public final class LauncherHelper {
suggestions.add(i18n("launch.advice.modlauncher8"));
break;
case VANILLA_X86:
if (!setting.get(GameSettings::useCustomNativesProperty)
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)
&& Platform.isSupportedTranslationX86_64()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
@@ -640,7 +640,7 @@ public final class LauncherHelper {
// Cannot allocate too much memory exceeding free space.
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
if (totalMemorySizeMB > 0 && !setting.getInheritable(GameSettings::autoMemoryProperty) && totalMemorySizeMB < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
}
@@ -358,10 +358,10 @@ public final class ModpackHelper {
GameSettings.PROPERTY_MAX_MEMORY,
GameSettings.PROPERTY_PERM_SIZE
));
setting.autoMemoryProperty().setValue(effective.get(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.get(GameSettings::minMemoryProperty));
setting.autoMemoryProperty().setValue(effective.getInheritable(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.getInheritable(GameSettings::minMemoryProperty));
setting.maxMemoryProperty().setValue(manifest.getLaunchInfo().getMinMemory());
setting.permSizeProperty().setValue(effective.get(GameSettings::permSizeProperty));
setting.permSizeProperty().setValue(effective.getInheritable(GameSettings::permSizeProperty));
}
});
}
@@ -39,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
@@ -54,6 +55,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@NotNullByDefault
public final class GameDirectoryManager {
/// The stable ID shared by newly created and migrated current-workspace game directories.
static final GameDirectoryID DEFAULT_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0x7105bc1f490e5e8cL, 0x878cf5844c3d4bc3L));
/// The stable ID shared by newly created and migrated user-home game directories.
static final GameDirectoryID HOME_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0xf3eafde8506e5a77L, 0xbc88f24b4728dfb2L));
/// The default current-workspace game directory path.
private static final PortablePath CURRENT_GAME_DIRECTORY_PATH = PortablePath.of(".minecraft");
@@ -154,12 +163,14 @@ public final class GameDirectoryManager {
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = localGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
}
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
@@ -216,9 +227,11 @@ public final class GameDirectoryManager {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
userGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
return true;
} else {
return false;
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
public sealed abstract class GameSettings extends ObservableSetting {
/// Suggested maximum heap memory in MiB.
static final int SUGGESTED_MEMORY;
public static final int SUGGESTED_MEMORY;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
@@ -331,6 +331,11 @@ public sealed abstract class GameSettings extends ObservableSetting {
}
/// Creates a new inheritable property.
protected final <T extends @Nullable Object> InheritableProperty<@Nullable T> newInheritableProperty(String name) {
return new SimpleInheritableProperty<>(this, name, null);
}
/// Creates a new inheritable property with the given default value.
protected final <T extends @UnknownNullability Object> InheritableProperty<T> newInheritableProperty(String name, T defaultValue) {
return new SimpleInheritableProperty<>(this, name, defaultValue);
}
@@ -388,10 +393,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized JVM options.
@SerializedName(PROPERTY_JVM_OPTIONS)
private final SettingProperty<String> jvmOptions = newSettingProperty(PROPERTY_JVM_OPTIONS, "");
private final InheritableProperty<String> jvmOptions = newInheritableProperty(PROPERTY_JVM_OPTIONS, "");
/// Returns the user customized JVM options property.
public SettingProperty<String> jvmOptionsProperty() {
public InheritableProperty<String> jvmOptionsProperty() {
return jvmOptions;
}
@@ -448,10 +453,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will automatically adjust memory allocation.
@SerializedName(PROPERTY_AUTO_MEMORY)
private final SettingProperty<Boolean> autoMemory = newSettingProperty(PROPERTY_AUTO_MEMORY, true);
private final InheritableProperty<Boolean> autoMemory = newInheritableProperty(PROPERTY_AUTO_MEMORY, true);
/// Returns the automatic memory allocation property.
public SettingProperty<Boolean> autoMemoryProperty() {
public InheritableProperty<Boolean> autoMemoryProperty() {
return autoMemory;
}
@@ -460,10 +465,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The minimum heap memory in MiB.
@SerializedName(PROPERTY_MIN_MEMORY)
private final SettingProperty<@Nullable Integer> minMemory = newSettingProperty(PROPERTY_MIN_MEMORY);
private final InheritableProperty<@Nullable Integer> minMemory = newInheritableProperty(PROPERTY_MIN_MEMORY);
/// Returns the minimum heap memory property.
public SettingProperty<@Nullable Integer> minMemoryProperty() {
public InheritableProperty<@Nullable Integer> minMemoryProperty() {
return minMemory;
}
@@ -472,10 +477,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The maximum heap memory in MiB.
@SerializedName(PROPERTY_MAX_MEMORY)
private final SettingProperty<@Nullable Integer> maxMemory = newSettingProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
private final InheritableProperty<@Nullable Integer> maxMemory = newInheritableProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
/// Returns the maximum heap memory property.
public SettingProperty<@Nullable Integer> maxMemoryProperty() {
public InheritableProperty<@Nullable Integer> maxMemoryProperty() {
return maxMemory;
}
@@ -484,10 +489,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The permanent generation or metaspace size in MiB.
@SerializedName(PROPERTY_PERM_SIZE)
private final SettingProperty<String> permSize = newSettingProperty(PROPERTY_PERM_SIZE, "");
private final InheritableProperty<String> permSize = newInheritableProperty(PROPERTY_PERM_SIZE, "");
/// Returns the permanent generation or metaspace size property.
public SettingProperty<String> permSizeProperty() {
public InheritableProperty<String> permSizeProperty() {
return permSize;
}
@@ -593,10 +598,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized arguments passed to the game.
@SerializedName(PROPERTY_GAME_ARGS)
private final SettingProperty<String> gameArguments = newSettingProperty(PROPERTY_GAME_ARGS, "");
private final InheritableProperty<String> gameArguments = newInheritableProperty(PROPERTY_GAME_ARGS, "");
/// Returns the customized game arguments property.
public SettingProperty<String> gameArgumentsProperty() {
public InheritableProperty<String> gameArgumentsProperty() {
return gameArguments;
}
@@ -643,10 +648,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized environment variables passed to the game.
@SerializedName(PROPERTY_ENVIRONMENT_VARIABLES)
private final SettingProperty<String> environmentVariables = newSettingProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
private final InheritableProperty<String> environmentVariables = newInheritableProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
/// Returns the customized environment variables property.
public SettingProperty<String> environmentVariablesProperty() {
public InheritableProperty<String> environmentVariablesProperty() {
return environmentVariables;
}
@@ -763,10 +768,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL does not patch native libraries.
@SerializedName(PROPERTY_NOT_PATCH_NATIVES)
private final SettingProperty<Boolean> notPatchNatives = newSettingProperty(PROPERTY_NOT_PATCH_NATIVES, false);
private final InheritableProperty<Boolean> notPatchNatives = newInheritableProperty(PROPERTY_NOT_PATCH_NATIVES, false);
/// Returns the native library patching property.
public SettingProperty<Boolean> notPatchNativesProperty() {
public InheritableProperty<Boolean> notPatchNativesProperty() {
return notPatchNatives;
}
@@ -775,10 +780,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL leaves native library management to the user.
@SerializedName(PROPERTY_USE_CUSTOM_NATIVES)
private final SettingProperty<Boolean> useCustomNatives = newSettingProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
private final InheritableProperty<Boolean> useCustomNatives = newInheritableProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
/// Returns the custom native libraries property.
public SettingProperty<Boolean> useCustomNativesProperty() {
public InheritableProperty<Boolean> useCustomNativesProperty() {
return useCustomNatives;
}
@@ -787,10 +792,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The path to the native library directory, or an empty string for the default path.
@SerializedName(PROPERTY_NATIVES_DIRECTORY)
private final SettingProperty<String> nativesDirectory = newSettingProperty(PROPERTY_NATIVES_DIRECTORY, "");
private final InheritableProperty<String> nativesDirectory = newInheritableProperty(PROPERTY_NATIVES_DIRECTORY, "");
/// Returns the native library directory property.
public SettingProperty<String> nativesDirectoryProperty() {
public InheritableProperty<String> nativesDirectoryProperty() {
return nativesDirectory;
}
@@ -799,10 +804,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native GLFW.
@SerializedName(PROPERTY_USE_NATIVE_GLFW)
private final SettingProperty<Boolean> useNativeGLFW = newSettingProperty(PROPERTY_USE_NATIVE_GLFW, false);
private final InheritableProperty<Boolean> useNativeGLFW = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW, false);
/// Returns the native GLFW property.
public SettingProperty<Boolean> useNativeGLFWProperty() {
public InheritableProperty<Boolean> useNativeGLFWProperty() {
return useNativeGLFW;
}
@@ -811,10 +816,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native OpenAL.
@SerializedName(PROPERTY_USE_NATIVE_OPENAL)
private final SettingProperty<Boolean> useNativeOpenAL = newSettingProperty(PROPERTY_USE_NATIVE_OPENAL, false);
private final InheritableProperty<Boolean> useNativeOpenAL = newInheritableProperty(PROPERTY_USE_NATIVE_OPENAL, false);
/// Returns the native OpenAL property.
public SettingProperty<Boolean> useNativeOpenALProperty() {
public InheritableProperty<Boolean> useNativeOpenALProperty() {
return useNativeOpenAL;
}
@@ -898,11 +903,6 @@ public sealed abstract class GameSettings extends ObservableSetting {
return instance;
}
/// Returns the effective value for a property whose override state is tracked by the property itself.
public <T extends @UnknownNullability Object> T get(Function<GameSettings, SettingProperty<T>> propertyGetter) {
return inherited(preset, instance, propertyGetter);
}
/// Returns the effective value for an inheritable property.
public <T extends @UnknownNullability Object> T getInheritable(
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
@@ -981,7 +981,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// Returns the effective maximum heap memory in MiB.
public int getMaxMemory() {
Integer value = get(GameSettings::maxMemoryProperty);
Integer value = getInheritable(GameSettings::maxMemoryProperty);
return value != null && value > 0 ? value : SUGGESTED_MEMORY;
}
@@ -87,6 +87,9 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Theme appearance override key for title-bar transparency.
public static final String THEME_APPEARANCE_TITLE_BAR_TRANSPARENT = "titleBarTransparent";
/// Theme appearance override key for window transparency.
public static final String THEME_APPEARANCE_WINDOW_TRANSPARENT = "windowTransparent";
/// Theme appearance override key for the primary background source.
public static final String THEME_APPEARANCE_BACKGROUND = "background";
@@ -336,6 +339,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
return titleBarTransparent;
}
/// Whether translucent launcher backgrounds reveal content behind the window.
@SerializedName("windowTransparent")
private final BooleanProperty windowTransparent = new SimpleBooleanProperty(false);
/// Returns the window transparency property.
public BooleanProperty windowTransparentProperty() {
return windowTransparent;
}
// Background source
/// The launcher background source type.
@@ -81,12 +81,6 @@ public final class LegacyConfigMigrator {
/// The legacy built-in profile name for the user-home game directory.
private static final String LEGACY_HOME_PROFILE = "Home";
/// The legacy built-in current-workspace profile ID.
private static final GameDirectoryID LEGACY_DEFAULT_PROFILE_ID = getLegacyProfileID(LEGACY_DEFAULT_PROFILE);
/// The legacy built-in user-home profile ID.
private static final GameDirectoryID LEGACY_HOME_PROFILE_ID = getLegacyProfileID(LEGACY_HOME_PROFILE);
/// The legacy Windows and portable configuration file name used before HMCL 3.16.
private static final String LEGACY_CONFIG_FILENAME = "hmcl.json";
@@ -261,6 +255,10 @@ public final class LegacyConfigMigrator {
renameMember(jsonObject, "bgpaint", "customBackgroundPaint");
renameMember(jsonObject, "backgroundPaint", "customBackgroundPaint");
migrateBackgroundOpacity(jsonObject, themeAppearanceOverrides);
jsonObject.addProperty("windowTransparent", true);
addThemeAppearanceOverride(
themeAppearanceOverrides,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT);
writeThemeAppearanceOverrides(jsonObject, themeAppearanceOverrides);
renameMember(jsonObject, "proxyUserName", "proxyUser");
migrateLegacySelectedVersions(jsonObject);
@@ -1359,10 +1357,10 @@ public final class LegacyConfigMigrator {
/// Returns the legacy profile name used in `configurations`.
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_DEFAULT_PROFILE;
}
if (LEGACY_HOME_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.HOME_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_HOME_PROFILE;
}
return GameDirectoryManager.getGameDirectoryCustomName(gameDirectory);
@@ -25,6 +25,9 @@ public enum ThemeColorType {
/// Uses the built-in launcher default theme color.
DEFAULT,
/// Follows the accent color provided by the operating system.
SYSTEM,
/// Uses the custom launcher theme color selected by the user.
CUSTOM,
@@ -0,0 +1,39 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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.theme;
import javafx.scene.layout.Background;
import org.jetbrains.annotations.NotNullByDefault;
import java.util.Objects;
/// A launcher background and the opacity applied by its rendering node.
///
/// @param background the JavaFX background rendered by the launcher
/// @param opacity the node opacity in the range `[0, 1]`
@NotNullByDefault
public record LauncherBackground(Background background, double opacity) {
/// Creates a launcher background value.
public LauncherBackground {
Objects.requireNonNull(background);
if (!Double.isFinite(opacity) || opacity < 0.0 || opacity > 1.0) {
throw new IllegalArgumentException("Launcher background opacity must be between 0 and 1: " + opacity);
}
}
}
@@ -43,6 +43,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
@NotNullByDefault
public record ThemeAppearance(
@Nullable ThemeColorSource color,
@@ -50,7 +51,8 @@ public record ThemeAppearance(
@Nullable ColorStyle colorStyle,
@Nullable Contrast contrast,
@Nullable ThemeBackgroundSettings background,
@Nullable ThemeTitleBar titleBar) {
@Nullable ThemeTitleBar titleBar,
@Nullable Boolean windowTransparent) {
/// JSON member name for the color seed.
static final String FIELD_COLOR = "color";
@@ -70,6 +72,9 @@ public record ThemeAppearance(
/// JSON member name for title-bar settings.
static final String FIELD_TITLE_BAR = "titleBar";
/// JSON member name for window transparency.
static final String FIELD_WINDOW_TRANSPARENT = "windowTransparent";
/// Creates an appearance patch.
///
/// @param color the color seed source, or `null` when inherited
@@ -78,6 +83,7 @@ public record ThemeAppearance(
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
public ThemeAppearance {
if (background != null && background.isEmpty()) {
background = null;
@@ -100,7 +106,8 @@ public record ThemeAppearance(
readColorStyle(object),
readContrast(object),
readBackground(object),
readTitleBar(object));
readTitleBar(object),
readWindowTransparent(object));
}
/// Returns whether this appearance contains no concrete fields.
@@ -112,7 +119,8 @@ public record ThemeAppearance(
&& colorStyle == null
&& contrast == null
&& background == null
&& titleBar == null;
&& titleBar == null
&& windowTransparent == null;
}
/// Adds this appearance patch's concrete fields to a JSON object.
@@ -139,6 +147,9 @@ public record ThemeAppearance(
if (titleBar != null) {
object.add(FIELD_TITLE_BAR, titleBar.toJsonObject());
}
if (windowTransparent != null) {
object.addProperty(FIELD_WINDOW_TRANSPARENT, windowTransparent);
}
}
/// Converts this appearance patch to its JSON representation.
@@ -167,7 +178,8 @@ public record ThemeAppearance(
: patch.background != null ? patch.background : background,
titleBar != null && patch.titleBar != null
? titleBar.merge(patch.titleBar)
: patch.titleBar != null ? patch.titleBar : titleBar);
: patch.titleBar != null ? patch.titleBar : titleBar,
patch.windowTransparent != null ? patch.windowTransparent : windowTransparent);
}
/// Converts this appearance to concrete launcher theme values.
@@ -309,6 +321,19 @@ public record ThemeAppearance(
}
}
/// Reads the optional window transparency field.
private static @Nullable Boolean readWindowTransparent(JsonObject object) {
JsonElement element = object.get(FIELD_WINDOW_TRANSPARENT);
if (element == null) {
return null;
}
if (!(element instanceof JsonPrimitive primitive) || !primitive.isBoolean()) {
LOG.warning("Ignored invalid theme windowTransparent: expected a boolean, got " + element);
return null;
}
return primitive.getAsBoolean();
}
/// Reads an optional string field.
private static @Nullable String readString(JsonObject object, String field) {
JsonElement element = object.get(field);
@@ -660,7 +660,8 @@ public final class ThemePackManager {
currentColorStyle(),
null,
background,
new ThemeTitleBar(currentTitleBarTransparent()));
new ThemeTitleBar(currentTitleBarTransparent()),
currentWindowTransparent());
Theme theme = new Theme(
null,
null,
@@ -711,6 +712,9 @@ public final class ThemePackManager {
if (themeColorType == ThemeColorType.DEFAULT) {
return ThemeColorSource.DEFAULT;
}
if (themeColorType == ThemeColorType.SYSTEM) {
return ThemeColorSource.custom(Themes.getSystemThemeColor());
}
if (themeColorType == ThemeColorType.BACKGROUND) {
if (backgroundType == BackgroundType.THEME_COLOR) {
return ThemeColorSource.DEFAULT;
@@ -755,6 +759,13 @@ public final class ThemePackManager {
: Objects.requireNonNullElse(resolveCurrentTitleBarTransparent(currentResolveContext()), false);
}
/// Returns whether the current launcher window should be transparent.
private static boolean currentWindowTransparent() throws IOException {
return SettingsManager.settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)
? settings().windowTransparentProperty().get()
: Objects.requireNonNullElse(resolveCurrentWindowTransparent(currentResolveContext()), false);
}
/// Resolves the selected theme's controlled brightness.
///
/// @param context the condition resolution context
@@ -795,6 +806,16 @@ public final class ThemePackManager {
return appearance != null && appearance.titleBar() != null ? appearance.titleBar().transparent() : null;
}
/// Resolves the selected theme window transparency directive.
///
/// @param context the condition resolution context
/// @return the selected theme window transparency directive, or `null` when unavailable
/// @throws IOException if the selected theme pack cannot be read
public static @Nullable Boolean resolveCurrentWindowTransparent(ThemeResolveContext context) throws IOException {
@Nullable ThemeAppearance appearance = resolveCurrentThemeAppearance(context);
return appearance != null ? appearance.windowTransparent() : null;
}
/// Resolves the current selected theme color source without extracting any wallpaper pixels.
///
/// @param context the condition resolution context
@@ -32,9 +32,6 @@ import javafx.collections.SetChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BackgroundImage;
@@ -115,6 +112,9 @@ public final class Themes {
if (FXUtils.DARK_MODE != null) {
observables.add(FXUtils.DARK_MODE);
}
if (FXUtils.ACCENT_COLOR != null) {
observables.add(FXUtils.ACCENT_COLOR);
}
bind(observables.toArray(new Observable[0]));
}
@@ -195,6 +195,7 @@ public final class Themes {
}
return switch (themeColorType) {
case DEFAULT -> ThemeColor.DEFAULT;
case SYSTEM -> getSystemThemeColor();
case CUSTOM -> fallback;
case BACKGROUND -> resolveWallpaperThemeColor(fallback, backgroundType);
};
@@ -282,9 +283,11 @@ public final class Themes {
colorScheme
);
/// The current JavaFX launcher background.
private static final ReadOnlyObjectWrapper<Background> background = new ReadOnlyObjectWrapper<>(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
/// The current JavaFX launcher background and its node opacity.
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
new LauncherBackground(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)),
1.0));
/// The last loaded background source used to rebuild the JavaFX background without reloading images.
private static @Nullable LoadedBackground loadedBackground;
@@ -300,8 +303,8 @@ public final class Themes {
/// A loaded JavaFX background source and the wallpaper color extracted while loading it.
private sealed interface LoadedBackground {
/// Returns the loaded JavaFX background.
Background background();
/// Returns the loaded JavaFX background and its node opacity.
LauncherBackground background();
/// Returns whether this background is the configured fallback background.
boolean fallbackBackground();
@@ -316,7 +319,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color extracted from [#image], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Image(
Background background,
LauncherBackground background,
javafx.scene.image.Image image,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -339,7 +342,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color represented by [#paint], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Paint(
Background background,
LauncherBackground background,
@Nullable javafx.scene.paint.Paint paint,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -359,7 +362,7 @@ public final class Themes {
/// @param background the loaded JavaFX background
/// @param fallbackBackground whether this background is the configured fallback background
record ThemeColorFill(
Background background,
LauncherBackground background,
boolean fallbackBackground) implements LoadedBackground {
/// Creates a loaded theme-color background.
///
@@ -528,6 +531,15 @@ public final class Themes {
return defaultBrightness = brightness;
}
/// Returns the current operating system accent color, or the launcher default when unavailable.
public static ThemeColor getSystemThemeColor() {
if (FXUtils.ACCENT_COLOR == null) {
return ThemeColor.DEFAULT;
}
@Nullable Color accentColor = FXUtils.ACCENT_COLOR.get();
return accentColor != null ? ThemeColor.of(accentColor) : ThemeColor.DEFAULT;
}
/// Returns the current resolved launcher theme.
public static ResolvedTheme getTheme() {
return theme.get();
@@ -543,8 +555,8 @@ public final class Themes {
return colorScheme.get();
}
/// Returns the current JavaFX launcher background property.
public static ReadOnlyObjectProperty<Background> backgroundProperty() {
/// Returns the current JavaFX launcher background and node opacity property.
public static ReadOnlyObjectProperty<LauncherBackground> backgroundProperty() {
startBackgroundUpdates();
return background.getReadOnlyProperty();
}
@@ -824,7 +836,8 @@ public final class Themes {
}
@Nullable LoadedBackground loaded = loadedBackground;
if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
Background newBackground = createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
LauncherBackground newBackground =
createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
loadedBackground = new LoadedBackground.ThemeColorFill(newBackground, themeColorFill.fallbackBackground());
background.set(newBackground);
}
@@ -858,17 +871,23 @@ public final class Themes {
LoadedBackground refreshed;
if (loaded instanceof LoadedBackground.Image imageBackground) {
refreshed = new LoadedBackground.Image(
createBackgroundWithOpacity(imageBackground.image(), opacity),
new LauncherBackground(
imageBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
imageBackground.image(),
imageBackground.wallpaperThemeColor(),
imageBackground.fallbackBackground());
} else if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
refreshed = new LoadedBackground.ThemeColorFill(
createThemeColorBackground(opacity),
new LauncherBackground(
themeColorFill.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
themeColorFill.fallbackBackground());
} else if (loaded instanceof LoadedBackground.Paint paintBackground) {
refreshed = new LoadedBackground.Paint(
createPaintBackground(paintBackground.paint(), opacity),
new LauncherBackground(
paintBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
paintBackground.paint(),
paintBackground.wallpaperThemeColor(),
paintBackground.fallbackBackground());
@@ -917,60 +936,37 @@ public final class Themes {
return FXUtils.loadImage(new CacheFileTask(url).run());
}
/// Creates a JavaFX paint background with the requested opacity.
private static Background createPaintBackground(@Nullable Paint paint, double opacity) {
opacity = MathUtils.clamp(opacity, 0., 1.);
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null) {
color = Color.WHITE;
}
if (opacity < 1.) {
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
}
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
} else {
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
}
/// Creates a JavaFX paint background with a separately stored node opacity.
private static LauncherBackground createPaintBackground(@Nullable Paint paint, double opacity) {
return new LauncherBackground(
new Background(new BackgroundFill(
Objects.requireNonNullElse(paint, Color.WHITE),
CornerRadii.EMPTY,
Insets.EMPTY)),
MathUtils.clamp(opacity, 0., 1.));
}
/// Creates a JavaFX background from the current theme color scheme surface container.
private static Background createThemeColorBackground(double opacity) {
private static LauncherBackground createThemeColorBackground(double opacity) {
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
}
/// Creates a JavaFX image background with the requested opacity.
private static Background createBackgroundWithOpacity(Image image, double opacity) {
PixelReader pixelReader = image.getPixelReader();
if (opacity <= 0) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
} else if (opacity >= 1. || pixelReader == null) {
return new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
} else {
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
PixelWriter pixelWriter = tempImage.getPixelWriter();
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
Color color = pixelReader.getColor(x, y);
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
pixelWriter.setColor(x, y, newColor);
}
}
return new Background(new BackgroundImage(
tempImage,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
}
/// Creates a JavaFX image background with a separately stored node opacity.
private static LauncherBackground createBackgroundWithOpacity(Image image, double opacity) {
Background imageBackground = new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(
BackgroundSize.AUTO,
BackgroundSize.AUTO,
false,
false,
false,
true)
));
return new LauncherBackground(imageBackground, MathUtils.clamp(opacity, 0., 1.));
}
/// Loads a default launcher background image from local workspace files.
@@ -1073,6 +1069,27 @@ public final class Themes {
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// Whether the launcher window should be transparent after applying launcher and theme settings.
private static final BooleanBinding windowTransparent = Bindings.createBooleanBinding(
() -> {
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)) {
return settings().windowTransparentProperty().get();
}
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
},
settings().windowTransparentProperty(),
settings().getThemeAppearanceOverrides(),
settings().selectedThemeProperty(),
settings().themeBrightnessModeProperty(),
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// The title text fill derived from the current color scheme.
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> titleBarTransparent.get()
@@ -1092,6 +1109,11 @@ public final class Themes {
return titleBarTransparent;
}
/// Returns whether the launcher window should be transparent after applying launcher and theme settings.
public static BooleanBinding windowTransparentProperty() {
return windowTransparent;
}
/// Returns whether the current color scheme uses dark brightness.
public static BooleanBinding darkModeProperty() {
return darkMode;
@@ -338,6 +338,13 @@ public class InstallerItem extends Control {
throw new AssertionError("Unknown state type: " + state.getClass());
}
}, control.resolvedStateProperty));
FXUtils.onChangeAndOperate(
control.resolvedStateProperty,
state -> statusLabel.pseudoClassStateChanged(
PseudoClass.getPseudoClass("incompatible"),
state instanceof IncompatibleState
)
);
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
@@ -121,6 +121,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
spinnerRefresh.setPrefSize(30.0, 30.0);
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot.tooltip"));
@@ -23,8 +23,8 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
@@ -43,11 +43,10 @@ import org.jackhuang.hmcl.util.MathUtils;
public class FloatScrollBarSkin implements Skin<ScrollBar> {
private ScrollBar scrollBar;
private Region group;
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
private final Rectangle track = new Rectangle();
private final Rectangle thumb = new Rectangle();
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
private ChangeListener<Boolean> thumbHoverListener;
private BooleanBinding hoverOrPressed;
private Animation thumbHoverAnimation;
public FloatScrollBarSkin(final ScrollBar scrollBar) {
@@ -214,7 +213,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
}
};
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
this.hoverOrPressed = thumb.hoverProperty().or(thumb.pressedProperty());
FXUtils.onChangeAndOperate(hoverOrPressed, newValue -> {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
@@ -249,7 +249,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
thumbHoverListener = null;
hoverOrPressed = null;
scrollBar = null;
group = null;
}
@@ -26,10 +26,12 @@ import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jetbrains.annotations.Nullable;
@@ -274,6 +276,8 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
/// Updates the fixed preferred height after the row width or subtitle content changes.
private void updatePreferredHeight() {
if (FXUtils.getLimitHeight(this) > 0) return;
double width = getWidth();
if (width <= 0) {
setMinHeight(MIN_HEIGHT);
@@ -53,8 +53,13 @@ public abstract class MDListCell<T> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) return;
updateControl(item, empty);
if (empty) {
setGraphic(null);
@@ -96,6 +96,12 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
});
}
/// Replaces the displayed choices.
@SafeVarargs
public final void setChoices(Choice<T>... choices) {
setChoices(List.of(choices));
}
/// Replaces the displayed choices.
public void setChoices(Collection<? extends Choice<T>> choices) {
this.choices.setAll(choices);
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.theme.LauncherBackground;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
@@ -49,7 +50,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class Decorator extends Control {
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
/// The launcher background and opacity rendered by this decorator's skin.
private final ObjectProperty<LauncherBackground> contentBackground = new SimpleObjectProperty<>();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
@@ -151,15 +154,18 @@ public class Decorator extends Control {
this.container.set(container);
}
public Background getContentBackground() {
/// Returns the launcher background rendered behind the content.
public LauncherBackground getContentBackground() {
return contentBackground.get();
}
public ObjectProperty<Background> contentBackgroundProperty() {
/// Returns the launcher background property rendered by this decorator's skin.
public ObjectProperty<LauncherBackground> contentBackgroundProperty() {
return contentBackground;
}
public void setContentBackground(Background contentBackground) {
/// Sets the launcher background rendered behind the content.
public void setContentBackground(LauncherBackground contentBackground) {
this.contentBackground.set(contentBackground);
}
@@ -44,6 +44,7 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.glavo.monetfx.ColorRole;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -134,9 +135,33 @@ public class DecoratorSkin extends SkinBase<Decorator> {
root.getChildren().setAll(shadowContainer);
StackPane wrapper = new StackPane();
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
() -> Themes.windowTransparentProperty().get()
? null
: new Background(new BackgroundFill(
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
CornerRadii.EMPTY,
Insets.EMPTY)),
Themes.windowTransparentProperty(),
Themes.colorSchemeProperty()));
Region backgroundNode = new Region();
backgroundNode.setMouseTransparent(true);
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
() -> skinnable.getContentBackground() == null
? null
: skinnable.getContentBackground().background(),
skinnable.contentBackgroundProperty()));
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
() -> skinnable.getContentBackground() == null
? 1.0
: skinnable.getContentBackground().opacity(),
skinnable.contentBackgroundProperty()));
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
BorderPane frame = new BorderPane();
frame.getStyleClass().addAll("jfx-decorator");
wrapper.getChildren().setAll(frame);
wrapper.getChildren().setAll(backgroundNode, frame);
skinnable.setDrawerWrapper(wrapper);
parent.getChildren().add(wrapper);
@@ -178,20 +203,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleContainer = new StackPane();
titleContainer.setPickOnBounds(false);
titleContainer.getStyleClass().addAll("jfx-tool-bar");
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
() -> Math.max(0.0, wrapper.getHeight()
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
wrapper.heightProperty(),
skinnable.titleTransparentProperty(),
titleContainer.heightProperty()));
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
// and decide whether the whole top bar should be rendered in white or black. TODO
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
if (titleTransparent) {
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
container.backgroundProperty().unbind();
container.setBackground(null);
titleContainer.getStyleClass().remove("background");
titleContainer.getStyleClass().add("gray-background");
} else {
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
wrapper.backgroundProperty().unbind();
wrapper.setBackground(null);
titleContainer.getStyleClass().add("background");
titleContainer.getStyleClass().remove("gray-background");
}
@@ -318,9 +318,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -61,11 +61,12 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
{
var archiveNamePane = new LinePane();
{
FXUtils.setLimitHeight(archiveNamePane, 75);
archiveNamePane.setTitle(i18n("version.name"));
txtModpackName = new JFXTextField();
txtModpackName.setPrefWidth(300);
FXUtils.setLimitHeight(archiveNamePane, 75);
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setRight(txtModpackName);
@@ -61,9 +61,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
})
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -213,6 +213,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
RemoteVersion oldRemoteVersion = getItem();
super.updateItem(remoteVersion, empty);
if (empty) {
@@ -221,6 +222,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
setGraphic(pane);
if (oldRemoteVersion == remoteVersion) return;
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
if (remoteVersion.getReleaseDate() != null) {
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
@@ -228,67 +231,65 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
twoLineListItem.setSubtitle(null);
}
if (oldRemoteVersion != remoteVersion) {
twoLineListItem.getTags().clear();
twoLineListItem.getTags().clear();
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
}
}
@@ -190,7 +190,7 @@ public final class ExportWizardProvider implements WizardProvider {
"OneSix",
exportInfo.getName() + "-" + exportInfo.getVersion(),
null,
Lang.toIntOrNull(setting.get(GameSettings::permSizeProperty)),
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
setting.getInheritable(GameSettings::commandWrapperProperty),
setting.getInheritable(GameSettings::preLaunchCommandProperty),
null,
@@ -101,9 +101,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName);
minMemory.set(Optional.ofNullable(versionSetting.get(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.get(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.get(GameSettings::jvmOptionsProperty));
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
canIncludeLauncher = JarUtils.thisJarPath() != null;
}
@@ -71,7 +71,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -107,6 +106,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
private boolean updatingParentSetting = false;
private final ObjectProperty<GameSettings.@Nullable Preset> activeParentSetting =
new SimpleObjectProperty<>(this, "activeParentSetting");
// GUI
private final ScrollPane scrollPane;
@@ -133,6 +134,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class;
this.isPresetSetting = settingType == GameSettings.Preset.class;
if (!isPresetSetting) {
bindActiveParentSetting();
}
this.scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -274,9 +278,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refreshJavaSettings();
});
SettingsManager.getGameSettings().addListener(weakJavaListener);
settings().defaultGameSettingsPresetProperty().addListener(weakJavaListener);
activeParentSetting.addListener(weakJavaListener);
javaItem.selectedChoiceProperty().addListener((observable, oldChoice, newChoice) -> {
S setting = currentSetting.get();
if (setting == null || updatingSelectedJava) {
@@ -362,6 +364,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
// Memory Setting
@Nullable JFXButton autoMemoryButton = !isPresetSetting ? createInheritanceButton() : null;
ObjectProperty<Boolean> memorySelectedValue = new SimpleObjectProperty<>(true);
var memorySublist = new ComponentSublist(() -> {
var memoryItem = new RadioChoiceList<Boolean>();
memoryItem.setFallbackValue(true);
@@ -381,16 +384,18 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton = createInheritanceButton();
}
var options = new ArrayList<RadioChoiceList.Choice<Boolean>>();
options.add(new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true));
var manualMemoryEditor = new HBox(8);
manualMemoryEditor.setAlignment(Pos.CENTER_RIGHT);
manualMemoryEditor.getChildren().setAll(
maxMemorySlider,
maxMemoryTextField,
new Label(i18n("settings.memory.unit.mib")));
options.add(new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton));
memoryItem.setChoices(options);
memoryItem.setChoices(List.of(
new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true),
new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton)
));
memoryItem.selectedValueProperty().addListener((observable, oldValue, newValue) ->
memorySelectedValue.set(newValue));
var memoryStatusBar = new MemoryStatusBar();
@@ -418,7 +423,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
return List.of(memoryItem, memoryStatusPane);
});
@@ -428,7 +434,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
gameSettings.getContent().add(memorySublist);
memorySublist.setTitle(i18n("settings.memory"));
memorySublist.setHasSubtitle(true);
memorySublist.setDescription(i18n("settings.memory.auto_allocate"));
memorySelectedValue.addListener((observable, oldValue, newValue) ->
memorySublist.setDescription(getMemoryAllocationDisplayName(newValue)));
memorySublist.setDescription(getMemoryAllocationDisplayName(memorySelectedValue.get()));
bindMemorySelectedValue(memorySelectedValue);
// Launcher Visibility Setting
var launcherVisibilityPane = createInheritableButton(
@@ -837,18 +846,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private void bindInstanceParentSetting(LineSelectButton<GameSettings.@Nullable Preset> button) {
ObservableList<GameSettings.Preset> items = FXCollections.observableArrayList();
ObservableList<GameSettings.Preset> presets = getAvailableParentPresets();
InvalidationListener updateItems = observable -> {
@Nullable GameSettings.Preset selected = button.getValue();
items.setAll((GameSettings.Preset) null);
items.addAll(SettingsManager.getGameSettings());
items.addAll(presets);
refreshInstanceParentSettingConverter(button);
if (selected != null && SettingsManager.getGameSettings(selected.idProperty().getValue()) == null) {
if (selected != null && getAvailableParentPreset(selected.idProperty().getValue()) == null) {
button.setValue(null);
}
};
InvalidationListener weakUpdateItems = holder.weak(updateItems);
updateItems.invalidated(SettingsManager.getGameSettings());
SettingsManager.getGameSettings().addListener(weakUpdateItems);
updateItems.invalidated(presets);
presets.addListener(weakUpdateItems);
button.setItems(items);
refreshInstanceParentSettingConverter(button);
@@ -865,7 +875,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
try {
refreshInstanceParentSettingConverter(button);
GameSettingsPresetID parent = setting.parentProperty().getValue();
button.setValue(parent != null ? SettingsManager.getGameSettings(parent) : null);
button.setValue(parent != null ? getAvailableParentPreset(parent) : null);
} finally {
updatingParentSetting = false;
}
@@ -880,6 +890,49 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
: i18n("settings.type.global.preset.default"));
}
/// Returns the presets that can be selected as an instance parent.
private ObservableList<GameSettings.Preset> getAvailableParentPresets() {
return SettingsManager.gameSettingsPresets().getPresets();
}
/// Returns the preset selected by an instance parent ID.
private GameSettings.@Nullable Preset getAvailableParentPreset(@Nullable GameSettingsPresetID id) {
return SettingsManager.gameSettingsPresets().getPreset(id);
}
/// Keeps [#activeParentSetting] synchronized with the current instance's resolved parent preset.
private void bindActiveParentSetting() {
InvalidationListener refresh = observable -> refreshActiveParentSetting();
InvalidationListener weakRefresh = holder.weak(refresh);
getAvailableParentPresets().addListener(weakRefresh);
SettingsManager.settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
if (newValue != null) {
newValue.addListener(weakRefresh);
}
refreshActiveParentSetting();
});
S setting = currentSetting.get();
if (setting != null) {
setting.addListener(weakRefresh);
}
refreshActiveParentSetting();
}
/// Refreshes the current instance's resolved parent preset.
private void refreshActiveParentSetting() {
activeParentSetting.set(currentSetting.get() instanceof GameSettings.Instance instance
? getEffectiveParentGameSettings(instance)
: null);
}
/// Adds the title-line inheritance button for the Java selection sublist.
private void bindJavaInheritanceButton(ComponentSublist sublist) {
if (isPresetSetting) {
@@ -1033,7 +1086,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds an integer text field to a setting property with independent override state.
@@ -1050,7 +1104,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds the windowed resolution selector to the width and height settings.
@@ -1093,6 +1148,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable Boolean> focusedListener = (observable, oldValue, newValue) -> {
if (!newValue) {
@@ -1178,8 +1234,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
widthProperty.addListener(weakPropertyListener);
heightProperty.addListener(weakPropertyListener);
}
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
propertyListener.invalidated(setting);
}
@@ -1416,7 +1470,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1434,6 +1492,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1499,9 +1558,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1524,7 +1580,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1542,6 +1602,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1607,9 +1668,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1728,6 +1786,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
GameSettings setting = currentSetting.get();
@@ -1779,9 +1838,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting instanceof GameSettings.Instance instance) {
instance.addListener(weakRefresh);
@@ -1825,13 +1881,96 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
}
/// Keeps a listener attached to the current instance's parent preset setting property.
private <T extends @UnknownNullability Object> void updateParentSettingPropertyListener(
@Nullable GameSettings setting,
ObjectProperty<@Nullable SettingProperty<T>> activeParentProperty,
Function<GameSettings, ? extends SettingProperty<T>> propertyGetter,
InvalidationListener listener) {
SettingProperty<T> oldParentProperty = activeParentProperty.get();
SettingProperty<T> newParentProperty = setting instanceof GameSettings.Instance instance
? propertyGetter.apply(getEffectiveParentGameSettings(instance))
: null;
if (oldParentProperty == newParentProperty) {
return;
}
if (oldParentProperty != null) {
oldParentProperty.removeListener(listener);
}
activeParentProperty.set(newParentProperty);
if (newParentProperty != null) {
newParentProperty.addListener(listener);
}
}
/// Binds the memory sublist description source to the effective automatic memory setting.
private void bindMemorySelectedValue(ObjectProperty<Boolean> memorySelectedValue) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
S setting = currentSetting.get();
updateParentSettingPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
refreshHolder.value);
if (setting == null) {
memorySelectedValue.set(true);
return;
}
memorySelectedValue.set(resolveEffectiveSetting(setting)
.getInheritable(GameSettings::autoMemoryProperty));
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
SettingProperty<Boolean> oldAutoMemoryProperty = activeAutoMemoryProperty.get();
if (oldAutoMemoryProperty != null) {
oldAutoMemoryProperty.removeListener(weakRefresh);
}
SettingProperty<Boolean> newAutoMemoryProperty = newValue != null
? newValue.autoMemoryProperty()
: null;
activeAutoMemoryProperty.set(newAutoMemoryProperty);
if (newValue != null) {
newValue.addListener(weakRefresh);
}
if (newAutoMemoryProperty != null) {
newAutoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(newValue);
});
S setting = currentSetting.get();
if (setting != null) {
SettingProperty<Boolean> autoMemoryProperty = setting.autoMemoryProperty();
activeAutoMemoryProperty.set(autoMemoryProperty);
setting.addListener(weakRefresh);
autoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(setting);
}
/// Binds a radio choice list to an inheritable setting property.
private <T> void bindInheritableRadioChoiceList(
ComponentSublist sublist,
RadioChoiceList<T> item,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1841,6 +1980,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1857,6 +2001,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable T> itemListener = (observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1921,9 +2067,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
propertyListener.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<T> property = propertyGetter.apply(setting);
@@ -1937,7 +2080,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Binds the window type editor to the window type setting.
private void bindWindowSettings(ComponentSublist sublist, RadioChoiceList<GameWindowType> item) {
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeWindowTypeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeParentWindowTypeProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1947,6 +2092,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentWindowTypeProperty,
GameSettings::windowTypeProperty,
refreshHolder.value);
InheritableProperty<GameWindowType> property = activeWindowTypeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1963,6 +2113,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
item.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2022,9 +2174,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
activeWindowTypeProperty.set(setting.windowTypeProperty());
@@ -2057,8 +2206,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private <T> void bindInheritableSublistDescription(ComponentSublist sublist,
Function<GameSettings, InheritableProperty<T>> propertyGetter,
Function<T, String> converter) {
InvalidationListener propertyListener = observable -> initInheritableSublistDescription(sublist, propertyGetter, converter);
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener propertyListener = observable -> {
updateParentInheritablePropertyListener(
currentSetting.get(),
activeParentProperty,
propertyGetter,
refreshHolder.value);
initInheritableSublistDescription(sublist, propertyGetter, converter);
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
@@ -2069,16 +2229,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyGetter.apply(newValue).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
propertyGetter.apply(setting).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(setting);
}
private <T> void initInheritableSublistDescription(ComponentSublist sublist,
@@ -2093,6 +2250,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
sublist.setDescription(converter.apply(getEffectiveValue(setting, propertyGetter)));
}
/// Returns the localized memory allocation mode display name.
private static String getMemoryAllocationDisplayName(Boolean autoMemory) {
return i18n(Boolean.TRUE.equals(autoMemory)
? "settings.memory.auto_allocate"
: "settings.memory.manual_allocate");
}
private static String getWindowTypeDisplayName(GameWindowType type) {
return switch (type) {
case FULLSCREEN -> i18n("settings.game.window_type.fullscreen");
@@ -2139,7 +2303,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
button.setOverriddenTooltip(i18n("settings.game.override_global"));
button.setInheritAvailable(!isPresetSetting);
IndependentSettingBinder.bindToggleButton(currentSetting, button, propertyGetter, holder, this::getEffectiveParentGameSettings);
IndependentSettingBinder.bindToggleButton(
currentSetting,
button,
propertyGetter,
holder,
this::getEffectiveParentGameSettings,
activeParentSetting);
return button;
}
@@ -2160,7 +2330,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineSelectButton<T> button,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -2170,6 +2342,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2186,6 +2359,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.valueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2251,9 +2426,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2268,10 +2440,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineInheritableToggleButton button,
Function<GameSettings, InheritableProperty<Boolean>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2280,7 +2455,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = true;
try {
boolean overridden = isPropertyOverridden(setting, property);
button.setRawValue(overridden ? getDirectValue(property) : getEffectiveValue(setting, propertyGetter));
button.setRawValue(overridden
? getDirectValue(property)
: getEffectiveValue(setting, propertyGetter));
button.setOverridden(overridden);
button.setEffectiveValue(getEffectiveValue(setting, propertyGetter));
} finally {
@@ -2288,6 +2465,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
InheritableProperty<Boolean> property = activeProperty.get();
@@ -2338,9 +2517,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2409,7 +2585,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Returns the explicitly configured parent preset for an instance, falling back to the default preset.
private GameSettings.Preset getExplicitParentGameSettings(GameSettings.Instance instance) {
GameSettingsPresetID parent = instance.parentProperty().getValue();
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
GameSettings.Preset parentSetting = getAvailableParentPreset(parent);
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -28,7 +29,6 @@ import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.property.SettingProperty;
import org.jackhuang.hmcl.ui.MemoryStatusBar;
@@ -48,7 +48,6 @@ import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -69,7 +68,8 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<String>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<String>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
@@ -84,7 +84,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(setting, activeParentProperty, propertyGetter, parentGetter, refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -104,6 +109,7 @@ final class IndependentSettingBinder {
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -161,9 +167,12 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Integer>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton;
if (presetSetting) {
inheritButton = null;
@@ -174,6 +183,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Integer> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -193,6 +208,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -257,12 +274,16 @@ final class IndependentSettingBinder {
@Nullable JFXButton maxMemoryButton,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeMaxMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentMaxMemoryProperty = new SimpleObjectProperty<>();
Label physicalMemoryLabel = (Label) memoryStatusLabels.getLeft();
Label allocatedMemoryLabel = (Label) memoryStatusLabels.getRight();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
int totalMemoryMiB = Math.max(1, (int) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
maxMemorySlider.setValueFactory(slider -> Bindings.createStringBinding(
@@ -271,6 +292,18 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
parentGetter,
refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentMaxMemoryProperty,
GameSettings::maxMemoryProperty,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> autoMemoryProperty = activeAutoMemoryProperty.get();
SettingProperty<Integer> maxMemoryProperty = activeMaxMemoryProperty.get();
if (setting == null || autoMemoryProperty == null || maxMemoryProperty == null || updating.value) {
@@ -299,6 +332,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
choiceList.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -433,8 +468,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
memoryStatusBar.memoryStatusProperty().addListener(observable -> {
GameSettings setting = currentSetting.get();
if (setting == null) {
@@ -468,12 +501,21 @@ final class IndependentSettingBinder {
LineInheritableToggleButton button,
Function<GameSettings, SettingProperty<Boolean>> propertyGetter,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -483,7 +525,9 @@ final class IndependentSettingBinder {
try {
boolean overridden = isOverridden(setting, property);
Boolean effectiveValue = getEffectiveValue(setting, propertyGetter, parentGetter);
button.setRawValue(overridden ? getDirectValue(property) : Boolean.TRUE.equals(effectiveValue));
button.setRawValue(overridden
? getDirectValue(property)
: Boolean.TRUE.equals(effectiveValue));
button.setOverridden(overridden);
button.setEffectiveValue(Boolean.TRUE.equals(effectiveValue));
} finally {
@@ -491,6 +535,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -503,7 +549,8 @@ final class IndependentSettingBinder {
try {
setOverridden(setting, property, true);
property.setValue(newValue);
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -522,7 +569,8 @@ final class IndependentSettingBinder {
if (newValue) {
property.setValue(button.getRawValue());
}
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -569,47 +617,26 @@ final class IndependentSettingBinder {
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
memoryStatusBar.memoryAllocatedProperty().set(calculateAllocatedMemory(memoryStatusBar, autoMemory, maxMemory));
updateMemoryLabels(memoryStatusBar, physicalMemoryLabel, allocatedMemoryLabel, autoMemory, maxMemory);
}
private static double calculateAllocatedMemory(
MemoryStatusBar memoryStatusBar,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
return HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatusBar.getMemoryStatus().available(),
Boolean.TRUE.equals(autoMemory));
}
private static void updateMemoryLabels(
MemoryStatusBar memoryStatusBar,
Label physicalMemoryLabel,
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
boolean autoMemoryEnabled = Boolean.TRUE.equals(autoMemory);
long memoryAllocated;
if (Boolean.TRUE.equals(autoMemory)) {
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available());
} else if (maxMemory != null && maxMemory > 0) {
memoryAllocated = maxMemory * 1024L * 1024L;
} else {
memoryAllocated = GameSettings.SUGGESTED_MEMORY * 1024L * 1024L;
}
memoryStatusBar.memoryAllocatedProperty().set(memoryAllocated);
physicalMemoryLabel.setText(i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.total())));
allocatedMemoryLabel.setText(i18n(
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.available()
? (autoMemoryEnabled
? "settings.memory.allocate.auto.exceeded"
: "settings.memory.allocate.manual.exceeded")
: (autoMemoryEnabled
? "settings.memory.allocate.auto"
: "settings.memory.allocate.manual"),
GIGABYTES.convertFromBytes(maxMemoryBytes),
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatus.available(),
autoMemoryEnabled)),
GIGABYTES.convertFromBytes(memoryStatus.available())));
allocatedMemoryLabel.setText(
memoryStatus.hasAvailable() && memoryAllocated > memoryStatus.available()
? i18n("settings.memory.allocate.exceeded", GIGABYTES.convertFromBytes(memoryAllocated), GIGABYTES.convertFromBytes(memoryStatus.available()))
: i18n("settings.memory.allocate", GIGABYTES.convertFromBytes(memoryAllocated)));
}
private static <T> void toggleOverride(
@@ -656,9 +683,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
GameSettings setting = currentSetting.get();
if (setting != null) {
SettingProperty<T> property = propertyGetter.apply(setting);
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -136,14 +137,17 @@ public final class AboutPage extends SpinnerPane {
if (obj.has("image")) {
JsonElement image = obj.get("image");
if (image.isJsonPrimitive()) {
button.setLeading(loadImage(image.getAsString()));
var imageView = new ImageContainer(32, 32);
imageView.setImage(loadImage(image.getAsString()));
imageView.setMouseTransparent(true);
button.setLeading(imageView);
} else if (image.isJsonObject()) {
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
);
}));
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
)));
}
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -33,14 +34,15 @@ import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
@@ -64,10 +66,10 @@ public class DownloadSettingsPage extends StackPane {
downloadSource.getStyleClass().add("card-non-transparent");
{
Function<DownloadSource, String> converter = source -> switch (source) {
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
Function<DownloadSource, String> descriptionConverter = source -> {
String bundleKey = switch (source) {
case DEFAULT -> "download.provider.balanced.desc";
@@ -106,91 +108,83 @@ public class DownloadSettingsPage extends StackPane {
{
var downloadList = new ComponentList();
VBox downloadThreads = new VBox(16);
ComponentSublist fileCommonLocationSublist = new ComponentSublist(() -> {
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
return List.of(fileCommonLocation);
});
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
{
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
ComponentSublist downloadThreadsSublist = new ComponentSublist(() -> {
var downloadThreadsList = new RadioChoiceList<Boolean>();
downloadThreadsList.setChoices(
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.auto"), true),
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.custom"), false) {
@Override
protected Node createRightNode() {
HBox hbox = new HBox(8);
hbox.setViewOrder(-1);
hbox.setAlignment(Pos.CENTER);
// hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
downloadThreads.getChildren().add(chkAutoDownloadThreads);
var changedByTextField = new Holder<>(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.value = true;
slider.setValue(value.intValue());
changedByTextField.value = false;
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.value) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
settings().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
hbox.getChildren().setAll(slider, threadsField);
return hbox;
}
}
});
);
downloadThreadsList.selectedValueProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
return List.of(downloadThreadsList);
});
downloadThreadsSublist.setTitle(i18n("settings.launcher.download.threads"));
downloadThreadsSublist.descriptionProperty().bind(Bindings.createStringBinding(() -> {
if (settings().autoDownloadThreadsProperty().get()) {
return i18n("settings.launcher.download.threads.auto");
} else {
return Integer.toString(settings().downloadThreadsProperty().get());
}
}, settings().autoDownloadThreadsProperty(), settings().downloadThreadsProperty()));
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
hbox.getChildren().setAll(label, slider, threadsField);
downloadThreads.getChildren().add(hbox);
}
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(settings().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
downloadThreads.getChildren().add(hintPane);
}
}
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreadsSublist);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
}
@@ -232,6 +232,7 @@ public final class JavaDownloadDialog extends StackPane {
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
this.packageTypeBox.setConverter(FXUtils.stringConverter(JavaPackageType::getDisplayName));
this.downloadButton = new JFXButton(i18n("download"));
downloadButton.setOnAction(e -> onDownload());
@@ -25,6 +25,7 @@ import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
@@ -226,6 +227,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
private static final class JavaItemCell extends ListCell<JavaRuntime> {
private final Node graphic;
private final Label label = new Label();
private final TwoLineListItem content;
private SVG removeIcon;
@@ -241,11 +243,17 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
label.setAlignment(Pos.CENTER);
label.setMinSize(24, 24);
label.setMaxSize(24, 24);
label.setPrefSize(24, 24);
label.setStyle("-fx-background-color: -monet-secondary-container; -fx-background-radius: 2; -fx-padding: 2; -fx-font-weight: normal; -fx-font-size: 12px;");
this.content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
BorderPane.setAlignment(content, Pos.CENTER);
center.getChildren().setAll(content);
center.getChildren().setAll(label, content);
root.setCenter(center);
HBox right = new HBox();
@@ -293,6 +301,9 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
if (empty || item == null) {
setGraphic(null);
} else {
int parsedVersion = item.getParsedVersion();
label.setText(parsedVersion >= 0 ? String.valueOf(parsedVersion) : "?");
content.setTitle((item.isJDK() ? "JDK" : "JRE") + " " + item.getVersion());
content.setSubtitle(item.getBinary().toString());
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.RotateTransition;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
@@ -29,6 +30,7 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
@@ -254,14 +256,37 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton = new JFXButton();
menuButton.getStyleClass().add("menu-button");
menuButton.setOnAction(e -> GameListPopupMenu.show(
menuButton.setOnAction(e -> {
JFXPopup popup = GameListPopupMenu.showAndGetPopup(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight(),
repository, versions
));
);
if (popup != null) {
Node graphic = menuButton.getGraphic();
if (graphic != null) {
if (AnimationUtils.isAnimationEnabled()) {
Duration duration = Duration.millis(200);
RotateTransition rotateOpen = new RotateTransition(duration, graphic);
rotateOpen.setToAngle(-180);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateOpen);
popup.setOnHidden(windowEvent -> {
RotateTransition rotateClose = new RotateTransition(duration, graphic);
rotateClose.setToAngle(0);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateClose);
});
} else {
graphic.setRotate(-180);
popup.setOnHidden(windowEvent -> graphic.setRotate(0));
}
}
}
});
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
@@ -23,6 +23,7 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -69,6 +70,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -213,6 +215,60 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
}
/// Binds a line toggle button to an inheritable theme appearance setting.
private static void bindThemeAppearanceToggleButton(
LineToggleButton button,
String setting,
BooleanProperty directProperty,
Supplier<Boolean> effectiveValueSupplier) {
JFXButton inheritButton = createThemeAppearanceOverrideButton();
button.setTitleTrailing(inheritButton);
Holder<Boolean> updating = new Holder<>(false);
InvalidationListener refresh = ignored -> {
if (updating.value) {
return;
}
updating.value = true;
try {
boolean overridden = settings().getThemeAppearanceOverrides().contains(setting);
button.setSelected(overridden ? directProperty.get() : effectiveValueSupplier.get());
updateThemeAppearanceOverrideButton(inheritButton, !overridden);
} finally {
updating.value = false;
}
};
button.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (updating.value) {
return;
}
updating.value = true;
try {
directProperty.set(Boolean.TRUE.equals(newValue));
settings().getThemeAppearanceOverrides().add(setting);
updateThemeAppearanceOverrideButton(inheritButton, false);
} finally {
updating.value = false;
}
refresh.invalidated(null);
});
directProperty.addListener(refresh);
addThemeAppearanceRefreshListener(refresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (!settings().getThemeAppearanceOverrides().contains(setting)) {
directProperty.set(button.isSelected());
settings().getThemeAppearanceOverrides().add(setting);
} else {
settings().getThemeAppearanceOverrides().remove(setting);
}
refresh.invalidated(null);
event.consume();
});
refresh.invalidated(null);
}
/// Returns the display name for the currently selected launcher theme.
private static String getSelectedThemeTitle() {
ThemeReference reference = settings().getSelectedThemeOrDefault();
@@ -323,10 +379,11 @@ public class PersonalizationPage extends StackPane {
private void exportCurrentThemePack(String packId, String version, String packName, String authorName) {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("theme_pack.export.title"));
String initialFileName = sanitizeThemePackFileName(packName) + ThemePackExporter.FILE_EXTENSION;
chooser.setInitialFileName(FileUtils.isNameValid(initialFileName)
? initialFileName
: "theme-pack" + ThemePackExporter.FILE_EXTENSION);
String initialFileName = sanitizeThemePackFileName(packName);
chooser.setInitialFileName(
(FileUtils.isNameValid(initialFileName) ? initialFileName : "theme-pack")
+ (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ThemePackExporter.FILE_EXTENSION)
);
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@@ -443,6 +500,10 @@ public class PersonalizationPage extends StackPane {
i18n("settings.launcher.theme_color_type.default"),
ThemeColorType.DEFAULT);
var systemColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.system"),
ThemeColorType.SYSTEM);
var customColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.custom"),
ThemeColorType.CUSTOM) {
@@ -459,7 +520,11 @@ public class PersonalizationPage extends StackPane {
RadioChoiceList<ThemeColorType> themeColorChoiceList = new RadioChoiceList<>();
themeColorChoiceList.setFallbackValue(ThemeColorType.DEFAULT);
themeColorChoiceList.setChoices(Arrays.asList(defaultColorChoice, customColorChoice, backgroundColorChoice));
themeColorChoiceList.setChoices(Arrays.asList(
defaultColorChoice,
systemColorChoice,
customColorChoice,
backgroundColorChoice));
JFXButton themeColorOverrideButton = createThemeAppearanceOverrideButton();
themeColorSublist.setTitleRight(themeColorOverrideButton);
@@ -1076,6 +1141,25 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
themeAppearanceList.getContent().add(titleBarTransparentButton);
}
LineToggleButton windowTransparentButton = new LineToggleButton();
windowTransparentButton.setTitle(i18n("settings.launcher.window_transparent"));
bindThemeAppearanceToggleButton(
windowTransparentButton,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT,
settings().windowTransparentProperty(),
() -> {
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(
ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
});
themeAppearanceList.getContent().add(windowTransparentButton);
content.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")),
themeAppearanceList,
@@ -273,30 +273,18 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
{
int rowIndex = 0;
if (control.versionSelection || !control.downloadSources.isEmpty()) {
if (control.versionSelection) {
searchPane.addRow(rowIndex);
int columns = 0;
Node lastNode = null;
if (control.versionSelection) {
JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
versionsComboBox.setMaxWidth(Double.MAX_VALUE);
Bindings.bindContent(versionsComboBox.getItems(), control.versions);
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion);
searchPane.add(new Label(i18n("version")), columns++, rowIndex);
searchPane.add(lastNode = versionsComboBox, columns++, rowIndex);
}
JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
versionsComboBox.setMaxWidth(Double.MAX_VALUE);
Bindings.bindContent(versionsComboBox.getItems(), control.versions);
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion);
if (control.downloadSources.getSize() > 1) {
JFXComboBox<String> downloadSourceComboBox = new JFXComboBox<>();
downloadSourceComboBox.setMaxWidth(Double.MAX_VALUE);
downloadSourceComboBox.getItems().setAll(control.downloadSources.get());
downloadSourceComboBox.setConverter(stringConverter(I18n::i18n));
selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource);
searchPane.add(new Label(i18n("settings.launcher.download_source")), columns++, rowIndex);
searchPane.add(lastNode = downloadSourceComboBox, columns++, rowIndex);
}
searchPane.add(new Label(i18n("version")), columns++, rowIndex);
searchPane.add(lastNode = versionsComboBox, columns++, rowIndex);
if (columns == 2) {
GridPane.setColumnSpan(lastNode, 3);
@@ -514,15 +502,23 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
spinnerPane.failedReasonProperty().bind(
Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) {
return i18n("download.failed.no_results_found");
} else {
return null;
}
},
getSkinnable().failedProperty(),
getSkinnable().loadingProperty(),
getSkinnable().pageCount,
getSkinnable().items)
);
spinnerPane.setOnFailedAction(e -> {
if (getSkinnable().retrySearch != null) {
if (getSkinnable().isFailed() && getSkinnable().retrySearch != null) {
getSkinnable().retrySearch.run();
}
});
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.setting.VersionIconType;
@@ -35,25 +37,35 @@ public class GameAdvancedListItem extends AdvancedListItem {
private final ImageContainer imageContainer;
private final WeakListenerHolder holder = new WeakListenerHolder();
private HMCLGameRepository repository;
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<RefreshedVersionsEvent> onRefreshedVersionsListener;
public GameAdvancedListItem() {
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
imageContainer.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion));
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), it -> this.loadVersion()));
}
private void loadVersion(String version) {
if (GameDirectoryManager.getSelectedRepository() != repository) {
private void loadVersion() {
String version = GameDirectoryManager.getSelectedInstance();
boolean repositoryChanged = GameDirectoryManager.getSelectedRepository() != repository;
if (repositoryChanged) {
repository = GameDirectoryManager.getSelectedRepository();
if (repository != null) {
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion(repository.getSelectedInstance());
});
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion();
});
if (!repository.isLoaded()) {
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.registerWeak(event -> loadVersion());
return;
}
}
if (version != null && repository != null && repository.hasVersion(version)) {
@@ -80,6 +80,7 @@ public final class GameListCell extends ListCell<GameListItem> {
{
HBox center = new HBox();
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
center.setMouseTransparent(true);
root.setCenter(center);
center.setPrefWidth(Region.USE_PREF_SIZE);
@@ -51,6 +51,12 @@ public final class GameListPopupMenu extends StackPane {
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
}
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream()
.filter(it -> repository.hasVersion(it.getId()))
@@ -58,6 +64,8 @@ public final class GameListPopupMenu extends StackPane {
.toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
return popup;
}
private final JFXListView<GameItem> listView = new JFXListView<>();
@@ -522,7 +522,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(24);
ImageContainer imageContainer = new ImageContainer(32);
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
@@ -471,7 +471,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
private final ResourcePackListPage page;
private final JFXCheckBox checkBox;
private final ImageContainer imageContainer = new ImageContainer(24);
private final ImageContainer imageContainer = new ImageContainer(32);
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER);
private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO);
@@ -314,8 +314,13 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
@Override
protected void updateItem(World world, boolean empty) {
World oldWorld = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(world, empty);
if (oldWorld == world && oldEmpty == empty) return;
this.content.getTags().clear();
if (empty || world == null) {
@@ -78,7 +78,7 @@ public final class NativePatcher {
JavaRuntime javaVersion,
GameSettings.Effective settings,
List<String> javaArguments) {
if (settings.get(GameSettings::useCustomNativesProperty)) {
if (settings.getInheritable(GameSettings::useCustomNativesProperty)) {
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
return version;
@@ -96,8 +96,8 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
final boolean useNativeGLFW = settings.get(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.get(GameSettings::useNativeOpenALProperty);
final boolean useNativeGLFW = settings.getInheritable(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.getInheritable(GameSettings::useNativeOpenALProperty);
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL)
&& gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) {
@@ -124,7 +124,7 @@ public final class NativePatcher {
Architecture arch = javaVersion.getArchitecture();
GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null;
if (settings.get(GameSettings::notPatchNativesProperty))
if (settings.getInheritable(GameSettings::notPatchNativesProperty))
return version;
if (arch.isX86() && (os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.MACOS))
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #282828;
-fixed-log-toggle-selected: #DEE2E6;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #FFFFFF;
-fixed-log-trace: #495057;
-fixed-log-debug: #343A40;
-fixed-log-info: #212529;
-fixed-log-warn: #331904;
-fixed-log-error: #58151C;
-fixed-log-fatal: #842029;
-fixed-log-selected: #6C757D;
-fixed-log-text-fill: #e3e3e3;
-fixed-log-trace-background: #495057;
-fixed-log-debug-background: #343A40;
-fixed-log-info-background: #212529;
-fixed-log-warn-background: #413c26;
-fixed-log-warn-text-fill: #fdf3aa;
-fixed-log-error-background: #4e3534;
-fixed-log-error-text-fill: #f9dedc;
-fixed-log-fatal-background: #842029;
-fixed-log-selected: #3e3e3e;
}
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #ffffff;
-fixed-log-toggle-selected: #000000;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #000000;
-fixed-log-trace: #EEE9E0;
-fixed-log-debug: #EEE9E0;
-fixed-log-info: #FFFFFF;
-fixed-log-warn: #FFEECC;
-fixed-log-error: #FFCCBB;
-fixed-log-fatal: #F7A699;
-fixed-log-selected: #C4C4C4;
-fixed-log-text-fill: #1f1f1f;
-fixed-log-trace-background: #EEE9E0;
-fixed-log-debug-background: #EEE9E0;
-fixed-log-info-background: #FFFFFF;
-fixed-log-warn-background: #fef6d5;
-fixed-log-warn-text-fill: #3e2f00;
-fixed-log-error-background: #fcebeb;
-fixed-log-error-text-fill: #410e0b;
-fixed-log-fatal-background: #F7A699;
-fixed-log-selected: #f2f2f2;
}
+33 -21
View File
@@ -396,6 +396,10 @@
-fx-cursor: hand;
}
.installer-item-status:incompatible {
-fx-opacity: 0.6;
}
.installer-item-wrapper {
-fx-background-color: -monet-surface;
-fx-background-radius: 4;
@@ -448,16 +452,24 @@
}
.launch-pane > .jfx-button.launch-button {
-fx-max-width: 207px;
-fx-min-width: 207px;
-fx-max-width: 200px;
-fx-min-width: 200px;
-fx-border-width: 0 3px 0 0;
-fx-border-color: -monet-on-surface-variant;
-fx-border-color: linear-gradient(
to bottom,
transparent 0%,
transparent 20%,
-monet-on-surface-variant 20%,
-monet-on-surface-variant 80%,
transparent 80%,
transparent 100%
);
-fx-background-radius: 4px 0 0 4px;
}
.launch-pane > .jfx-button.menu-button {
-fx-max-width: 20px;
-fx-min-width: 20px;
-fx-max-width: 27px;
-fx-min-width: 27px;
-fx-font-size: 15px;
-fx-background-radius: 0 4px 4px 0;
}
@@ -1382,27 +1394,27 @@
}
.log-toggle.fatal {
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-toggle.error {
-fx-background-color: -fixed-log-error;
-fx-background-color: -fixed-log-error-background;
}
.log-toggle.warn {
-fx-background-color: -fixed-log-warn;
-fx-background-color: -fixed-log-warn-background;
}
.log-toggle.info {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-toggle.debug {
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-toggle.trace {
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -1410,7 +1422,7 @@
}
.log-window {
-fx-background-color: -monet-surface-container;
-fx-background-color: -fixed-log-background;
}
.log-window .scroll-bar .thumb {
@@ -1433,36 +1445,36 @@
.log-window-list-cell:fatal {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-window-list-cell:error {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-error;
-fx-text-fill: -fixed-log-error-text-fill;
-fx-background-color: -fixed-log-error-background;
}
.log-window-list-cell:warn {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-warn;
-fx-text-fill: -fixed-log-warn-text-fill;
-fx-background-color: -fixed-log-warn-background;
}
.log-window-list-cell:info {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell:debug {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-window-list-cell:trace {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -399,6 +399,7 @@ download.external_link=Visit Download Website
download.failed=Failed to download "%1$s", response code: %2$d.
download.failed.empty=No versions are available. Please click here to go back.
download.failed.no_code=Failed to download
download.failed.no_results_found=No results found
download.failed.refresh=Failed to fetch version list. Please click here to retry.
download.game=New Game
download.provider.bmclapi=BMCLAPI & MCIM
@@ -487,7 +488,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
\n\
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" or "Mesa Lavapipe" in "Global/Instance-specific Settings → Graphics Settings" to use CPU rendering for better compatibility.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
@@ -1535,7 +1536,7 @@ settings.game_directories.read_only=The game directory file was saved by a newer
settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience
settings.launcher.animation=Animation
settings.launcher.appearance=Appearance
settings.launcher.brightness=Theme Mode
@@ -1547,8 +1548,9 @@ settings.launcher.debug=Debug
settings.launcher.disable_april_fools=Do not enable April Fools features
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Threads
settings.launcher.download.threads=Download Threads
settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.custom=Custom
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
@@ -1604,16 +1606,16 @@ settings.launcher.theme_color_type.background=Follow Background Wallpaper
settings.launcher.theme_color_type.custom=Custom Color
settings.launcher.theme_color_type.custom.description=Use the color selected above.
settings.launcher.theme_color_type.default=Default
settings.launcher.theme_color_type.system=Follow System
settings.launcher.title_transparent=Transparent Titlebar
settings.launcher.turn_off_animations=Disable Animation
settings.launcher.version_list_source=Version List
settings.launcher.window_transparent=Transparent Window
settings.launcher.background.settings.opacity=Opacity
settings.memory=Memory
settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated
settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate.manual=%1$.1f GiB Allocated
settings.memory.allocate.manual.exceeded=%1$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate=%1$.1f GiB Allocated
settings.memory.allocate.exceeded=%1$.1f GiB Allocated (%2$.1f GiB Available)
settings.memory.auto_allocate=Automatically Allocate
settings.memory.lower_bound=Minimum Memory
settings.memory.manual_allocate=Manually Allocate Memory
@@ -1428,10 +1428,8 @@ settings.launcher.version_list_source=قائمة الإصدارات
settings.launcher.background.settings.opacity=الشفافية
settings.memory=الذاكرة
settings.memory.allocate.auto=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص
settings.memory.allocate.auto.exceeded=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate.manual=%1$.1f جيجابايت مخصص
settings.memory.allocate.manual.exceeded=%1$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate=%1$.1f جيجابايت مخصص
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
settings.memory.auto_allocate=تخصيص تلقائي
settings.memory.lower_bound=الحد الأدنى للذاكرة
settings.memory.unit.mib=ميجابايت
@@ -1329,10 +1329,8 @@ settings.launcher.version_list_source=Lista de versiones
settings.launcher.background.settings.opacity=Opacidad
settings.memory=Memoria
settings.memory.allocate.auto=%1$.1f GiB Mínimo / %2$.1f GiB Asignado
settings.memory.allocate.auto.exceeded=%1$.1f GiB Mínimo / %2$.1f GiB Asignado (%3$.1f GiB Disponible)
settings.memory.allocate.manual=%1$.1f GiB Asignados
settings.memory.allocate.manual.exceeded=%1$.1f GiB Asignados (%3$.1f GiB Disponibles)
settings.memory.allocate=%1$.1f GiB Asignados
settings.memory.allocate.exceeded=%1$.1f GiB Asignados (%2$.1f GiB Disponibles)
settings.memory.auto_allocate=Asignar automáticamente
settings.memory.lower_bound=Memoria mínima
settings.memory.unit.mib=MiB
@@ -856,10 +856,8 @@ settings.launcher.title_transparent=タイトルの透明性
settings.launcher.version_list_source=バージョンリストソース
settings.memory=ゲームメモリ
settings.memory.allocate.auto=最小 %1$.1f GiB /実際の %2$.1f GiB
settings.memory.allocate.auto.exceeded=最小 %1$.1f GiB /実際の %2$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate.manual=割り当て %1$.1f GiB
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate=割り当て %1$.1f GiB
settings.memory.allocate.exceeded=割り当て %1$.1f GiB(%2$.1f GiB使用可能)
settings.memory.auto_allocate=メモリサイズの自動割り当て
settings.memory.lower_bound=最小メモリ
settings.memory.unit.mib=MiB
@@ -1110,7 +1110,7 @@ settings.icon=戲徵
settings.launcher=啟者置設
settings.launcher.allow_auto_agent=許改戲
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
settings.launcher.debug=勘誤
@@ -1152,10 +1152,8 @@ settings.launcher.version_list_source=版列供者
settings.launcher.background.settings.opacity=陰翳
settings.memory=戲憶
settings.memory.allocate.auto=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB
settings.memory.allocate.auto.exceeded=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate.manual=戲分之 %1$.1f GiB
settings.memory.allocate.manual.exceeded=戲分之 %1$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate=戲分之 %1$.1f GiB
settings.memory.allocate.exceeded=戲分之 %1$.1f GiB (械祇 %2$.1f GiB)
settings.memory.auto_allocate=自調其憶
settings.memory.lower_bound=至低憶界
settings.memory.unit.mib=兆二進字節
@@ -1318,10 +1318,8 @@ settings.launcher.version_list_source=Список версий
settings.launcher.background.settings.opacity=Непрозрачность
settings.memory=Оперативная память
settings.memory.allocate.auto=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ
settings.memory.allocate.auto.exceeded=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.allocate.manual=Выделено %1$.1f ГиБ
settings.memory.allocate.manual.exceeded=Выделено %1$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.allocate=Выделено %1$.1f ГиБ
settings.memory.allocate.exceeded=Выделено %1$.1f ГиБ (Доступно %2$.1f ГиБ)
settings.memory.auto_allocate=Автовыделение
settings.memory.lower_bound=Мин. объём памяти
settings.memory.unit.mib=МиБ
@@ -1267,10 +1267,8 @@ settings.launcher.version_list_source=Список версій
settings.launcher.background.settings.opacity=Непрозорість
settings.memory=Пам'ять
settings.memory.allocate.auto=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено
settings.memory.allocate.auto.exceeded=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
settings.memory.allocate.manual=%1$.1f ГіБ Виділено
settings.memory.allocate.manual.exceeded=%1$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
settings.memory.allocate=%1$.1f ГіБ Виділено
settings.memory.allocate.exceeded=%1$.1f ГіБ Виділено (%2$.1f ГіБ Доступно)
settings.memory.auto_allocate=Автоматично виділяти
settings.memory.lower_bound=Мінімальна пам'ять
settings.memory.unit.mib=МіБ
@@ -397,6 +397,7 @@ download.external_link=開啟下載網站
download.failed=下載失敗:%1$s\n錯誤碼:%2$d
download.failed.empty=沒有可供安裝的版本,按一下此處返回。
download.failed.no_code=下載失敗
download.failed.no_results_found=搜尋無結果
download.failed.refresh=載入版本清單失敗,按一下此處重試。
download.game=新遊戲
download.provider.bmclapi=BMCLAPI & MCIM
@@ -457,7 +458,7 @@ fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 圖形設定」中將「繪製器」切換為「Mesa LLVMpipe」/「Mesa Lavapipe」,以獲得更好的相容性。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
# When translating the following string into non-Latin languages, please retain the English translation
@@ -1326,7 +1327,7 @@ settings.game_directories.read_only=遊戲目錄設定檔是由更高版本的 H
settings.launcher=啟動器設定
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗
settings.launcher.animation=动画
settings.launcher.appearance=外觀
settings.launcher.brightness=主題模式
@@ -1338,8 +1339,9 @@ settings.launcher.debug=除錯
settings.launcher.disable_april_fools=不啟用愚人節功能
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
settings.launcher.download=下載
settings.launcher.download.threads=執行緒數
settings.launcher.download.threads=下載執行緒數
settings.launcher.download.threads.auto=自動選取執行緒數
settings.launcher.download.threads.custom=自訂執行緒數
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
@@ -1395,16 +1397,16 @@ settings.launcher.theme_color_type.background=跟隨背景圖片
settings.launcher.theme_color_type.custom=自訂顏色
settings.launcher.theme_color_type.custom.description=使用上方選擇的顏色。
settings.launcher.theme_color_type.default=預設
settings.launcher.theme_color_type.system=跟隨系統
settings.launcher.title_transparent=標題欄透明
settings.launcher.turn_off_animations=關閉動畫
settings.launcher.version_list_source=版本清單來源
settings.launcher.window_transparent=視窗透明
settings.launcher.background.settings.opacity=不透明度
settings.memory=遊戲記憶體
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate.manual=遊戲分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=遊戲分配 %1$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate=遊戲分配 %1$.1f GiB
settings.memory.allocate.exceeded=遊戲分配 %1$.1f GiB (%2$.1f GiB 可用)
settings.memory.auto_allocate=自動分配
settings.memory.lower_bound=最低分配
settings.memory.manual_allocate=手動選取記憶體
@@ -399,6 +399,7 @@ download.external_link=打开下载网站
download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。
download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助)
download.failed.no_code=下载失败
download.failed.no_results_found=搜索无结果
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
download.game=新游戏
download.provider.bmclapi=BMCLAPI & MCIM
@@ -460,7 +461,7 @@ fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的运行路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 图形设置”中将“渲染器”切换为“Mesa LLVMpipe” / “Mesa Lavapipe”,以获得更好的兼容性。\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -1331,7 +1332,7 @@ settings.game_directories.read_only=游戏文件夹配置文件是由更高版
settings.launcher=启动器设置
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验
settings.launcher.animation=动画
settings.launcher.appearance=外观
settings.launcher.brightness=主题模式
@@ -1343,8 +1344,9 @@ settings.launcher.debug=调试
settings.launcher.disable_april_fools=不启用愚人节功能
settings.launcher.disable_auto_game_options=不自动切换游戏语言
settings.launcher.download=下载
settings.launcher.download.threads=线程数
settings.launcher.download.threads=下载线程数
settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.custom=自定义线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
@@ -1400,16 +1402,16 @@ settings.launcher.theme_color_type.background=跟随背景壁纸
settings.launcher.theme_color_type.custom=自定义颜色
settings.launcher.theme_color_type.custom.description=使用上方选择的颜色。
settings.launcher.theme_color_type.default=默认
settings.launcher.theme_color_type.system=跟随系统
settings.launcher.title_transparent=标题栏透明
settings.launcher.turn_off_animations=关闭动画
settings.launcher.version_list_source=版本列表源
settings.launcher.window_transparent=窗口透明
settings.launcher.background.settings.opacity=不透明度
settings.memory=游戏内存
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate.manual=游戏分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=游戏分配 %1$.1f GiB (设备仅 %3$.1f GiB 可用)
settings.memory.allocate=游戏分配 %1$.1f GiB
settings.memory.allocate.exceeded=游戏分配 %1$.1f GiB (设备仅 %2$.1f GiB 可用)
settings.memory.auto_allocate=自动分配内存
settings.memory.lower_bound=最低内存分配
settings.memory.manual_allocate=手动选择内存
@@ -96,8 +96,8 @@ public final class GameDirectoriesTest {
GameDirectories gameDirectories = Objects.requireNonNull(LegacyConfigMigrator.extractGameDirectoriesFromConfigJson(settings));
GameDirectoryID defaultGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Default");
GameDirectoryID homeGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Home");
GameDirectoryID defaultGameDirectoryId = GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID;
GameDirectoryID homeGameDirectoryId = GameDirectoryManager.HOME_GAME_DIRECTORY_ID;
GameDirectory defaultGameDirectory = gameDirectories.getGameDirectories().stream()
.filter(gameDirectory -> defaultGameDirectoryId.equals(gameDirectory.getId()))
.findFirst()
@@ -357,12 +357,36 @@ public final class GameDirectoriesTest {
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
userDirectories,
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
assertEquals(2, GameDirectoryManager.getGameDirectories().size());
assertTrue(GameDirectoryManager.getGameDirectories().contains(localGameDirectory));
assertTrue(GameDirectoryManager.getGameDirectories().contains(userGameDirectory));
}
}
/// Tests that newly created stores use the stable IDs for their built-in game directories.
@Test
public void createsBuiltInGameDirectoriesWithStableIdsForNewStores() throws ReflectiveOperationException {
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
userDirectories.setNewlyCreated(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.setUserFile(false);
localDirectories.setNewlyCreated(true);
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
GameDirectory localGameDirectory = assertSingleDefaultGameDirectory(localDirectories, PortablePath.of(".minecraft"));
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
userDirectories,
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
}
}
/// Tests that the selected game directory getter restores a valid selection instead of returning null.
@Test
public void selectedGameDirectoryGetterNeverReturnsNullWhenGameDirectoriesAreLoaded() throws ReflectiveOperationException {
@@ -408,6 +432,44 @@ public final class GameDirectoriesTest {
}
}
/// Tests that new isolated installing instances resolve content directories under the version root before metadata is saved.
@Test
public void newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists(@TempDir Path tempDirectory)
throws ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002");
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(defaultPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()));
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
String id = "1.21.11-fabric";
assertFalse(repository.hasVersion(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
repository.applyDefaultIsolationSettingForNewInstance(id, true);
assertEquals(repository.getVersionRoot(id), repository.getRunDirectory(id));
assertEquals(repository.getVersionRoot(id).resolve("mods"), repository.getModsDirectory(id));
assertTrue(repository.removeVersionFromDisk(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
}
}
/// Tests that instance settings without an explicit parent use the default preset.
@Test
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()
@@ -137,6 +137,7 @@ public final class LauncherSettingsMigrationTest {
assertEquals(ThemeColor.of("#336699"), launcherSettings.customThemeColorProperty().get());
assertEquals(ThemeColorType.CUSTOM, launcherSettings.themeColorTypeProperty().get());
assertTrue(launcherSettings.titleBarTransparentProperty().get());
assertTrue(launcherSettings.windowTransparentProperty().get());
assertEquals("/pictures/background.png", launcherSettings.customBackgroundImagePathProperty().get());
assertEquals("https://example.com/background.png",
launcherSettings.networkBackgroundImageUrlProperty().get());
@@ -148,6 +149,8 @@ public final class LauncherSettingsMigrationTest {
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_COLOR));
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
LauncherSettings.THEME_APPEARANCE_TITLE_BAR_TRANSPARENT));
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT));
assertFalse(launcherSettings.getThemeAppearanceOverrides().contains("networkBackgroundImageCachePolicy"));
assertEquals("/pictures/background.png", serialized.get("customBackgroundImagePath").getAsString());
assertEquals("https://example.com/background.png",
@@ -158,6 +161,7 @@ public final class LauncherSettingsMigrationTest {
assertEquals(ThemeColorType.CUSTOM.name(), serialized.get("themeColorType").getAsString());
assertEquals("dark", serialized.get("themeBrightnessMode").getAsString());
assertTrue(serialized.get("titleBarTransparent").getAsBoolean());
assertTrue(serialized.get("windowTransparent").getAsBoolean());
assertEquals("#336699", serialized.get("customBackgroundPaint").getAsString());
}
}
@@ -0,0 +1,54 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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.theme;
import javafx.geometry.Insets;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
/// Tests launcher background opacity storage.
@NotNullByDefault
public final class LauncherBackgroundTest {
/// Verifies opacity changes reuse the unmodified JavaFX background.
@Test
public void changesOpacityWithoutChangingBackground() {
Background background = new Background(
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
LauncherBackground changed = new LauncherBackground(background, 0.4);
assertSame(background, changed.background());
assertEquals(0.4, changed.opacity());
assertEquals(1.0, ((Color) changed.background().getFills().get(0).getFill()).getOpacity());
}
/// Verifies invalid node opacity values are rejected.
@Test
public void rejectsInvalidOpacity() {
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, -0.1));
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, 1.1));
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, Double.NaN));
}
}
@@ -86,6 +86,7 @@ public final class ThemePackManifestTest {
assertEquals(BackgroundType.DEFAULT, background.type());
assertEquals(1.0, background.opacity(), 0.0);
assertNull(theme.appearance().windowTransparent());
}
}
@@ -21,15 +21,17 @@ package org.jackhuang.hmcl.download.java;
* @author Glavo
*/
public enum JavaPackageType {
JRE(false, false),
JDK(true, false),
JREFX(false, true),
JDKFX(true, true);
JRE("JRE", false, false),
JDK("JDK", true, false),
JREFX("JRE FX", false, true),
JDKFX("JDK FX", true, true);
private final String displayName;
private final boolean jdk;
private final boolean javafx;
JavaPackageType(boolean jdk, boolean javafx) {
JavaPackageType(String displayName, boolean jdk, boolean javafx) {
this.displayName = displayName;
this.jdk = jdk;
this.javafx = javafx;
}
@@ -41,6 +43,10 @@ public enum JavaPackageType {
return javafx ? JREFX : JRE;
}
public String getDisplayName() {
return displayName;
}
public boolean isJDK() {
return jdk;
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -85,12 +86,17 @@ public final class QuiltInstallTask extends Task<Version> {
@Override
public void execute() {
setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), QuiltInfo.class), remote.getGameVersion(), remote.getSelfVersion()));
setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), QuiltInfo.class), remote.getSelfVersion()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
private Version getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderVersion) {
/// Creates the Quilt version patch described by the metadata response.
///
/// @param quiltInfo the metadata returned by Quilt Meta
/// @param loaderVersion the Quilt Loader version
/// @return the version patch to install
static Version getPatch(QuiltInfo quiltInfo, String loaderVersion) {
JsonObject launcherMeta = quiltInfo.launcherMeta;
Arguments arguments = new Arguments();
@@ -118,7 +124,9 @@ public final class QuiltInstallTask extends Task<Version> {
}
// libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.hashed.maven), getMavenRepositoryByGroup(quiltInfo.hashed.maven), null));
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null));
if (quiltInfo.intermediary != null) {
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null));
}
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.loader.maven), getMavenRepositoryByGroup(quiltInfo.loader.maven), null));
return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries);
@@ -139,11 +147,11 @@ public final class QuiltInstallTask extends Task<Version> {
@JsonSerializable
public static class QuiltInfo {
private final LoaderInfo loader;
private final IntermediaryInfo hashed;
private final IntermediaryInfo intermediary;
private final @Nullable IntermediaryInfo hashed;
private final @Nullable IntermediaryInfo intermediary;
private final JsonObject launcherMeta;
public QuiltInfo(LoaderInfo loader, IntermediaryInfo hashed, IntermediaryInfo intermediary, JsonObject launcherMeta) {
public QuiltInfo(LoaderInfo loader, @Nullable IntermediaryInfo hashed, @Nullable IntermediaryInfo intermediary, JsonObject launcherMeta) {
this.loader = loader;
this.hashed = hashed;
this.intermediary = intermediary;
@@ -154,11 +162,11 @@ public final class QuiltInstallTask extends Task<Version> {
return loader;
}
public IntermediaryInfo getHashed() {
public @Nullable IntermediaryInfo getHashed() {
return hashed;
}
public IntermediaryInfo getIntermediary() {
public @Nullable IntermediaryInfo getIntermediary() {
return intermediary;
}
@@ -233,7 +233,7 @@ public class DefaultGameRepository implements GameRepository {
public boolean removeVersionFromDisk(String id) {
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
return false;
if (!versions.containsKey(id))
if (versions == null || !versions.containsKey(id))
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
Path file = getVersionRoot(id);
if (Files.notExists(file))
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,7 +27,7 @@ import java.util.Map;
/// A wrapper for `Map<String, Object>`, supporting type-safe reading and writing of values.
///
/// @author Glavo
/// @author Glavo
public final class SettingsMap {
public record Key<T>(String key) {
}
@@ -87,6 +89,19 @@ public final class SettingsMap {
map.clear();
}
/// Returns whether the selected installation includes any non-vanilla component.
public boolean isInstallingModdedVersion() {
for (LibraryAnalyzer.LibraryType value : LibraryAnalyzer.LibraryType.values()) {
if (value != LibraryAnalyzer.LibraryType.MINECRAFT
&& value.isModLoader()
&& get(value.getPatchId()) instanceof RemoteVersion) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "SettingsMap" + map;
@@ -131,78 +131,105 @@ public abstract class HttpRequest {
return con;
}
public static class HttpGetRequest extends HttpRequest {
protected HttpGetRequest(String url) {
super(url, "GET");
protected void checkResponseCode(HttpURLConnection con) throws IOException {
int code = con.getResponseCode();
if (code / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(code)) {
try {
throw new ResponseCodeException(this.url, code, NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new ResponseCodeException(this.url, code, e);
}
}
}
}
public static abstract class HttpSimpleRequest extends HttpRequest {
protected HttpSimpleRequest(String url, String method) {
super(url, method);
}
@Override
public String getString() throws IOException {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con = resolveConnection(con);
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(con.getInputStream()) : con.getInputStream());
checkResponseCode(con);
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding())
? IOUtils.wrapFromGZip(con.getInputStream())
: con.getInputStream());
}, retryTimes);
}
}
public static final class HttpPostRequest extends HttpRequest {
private byte[] bytes;
public static class HttpGetRequest extends HttpSimpleRequest {
protected HttpGetRequest(String url) { super(url, "GET"); }
}
private HttpPostRequest(String url) {
super(url, "POST");
public static class HttpDeleteRequest extends HttpSimpleRequest {
protected HttpDeleteRequest(String url) { super(url, "DELETE"); }
}
@SuppressWarnings("unchecked")
public static abstract class HttpEntityRequest<T extends HttpEntityRequest<T>> extends HttpRequest {
protected byte[] bytes;
protected HttpEntityRequest(String url, String method) {
super(url, method);
}
public HttpPostRequest contentType(String contentType) {
public T contentType(String contentType) {
headers.put("Content-Type", contentType);
return this;
return (T) this;
}
public HttpPostRequest json(Object payload) throws JsonParseException {
public T json(Object payload) throws JsonParseException {
return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
}
public HttpPostRequest form(Map<String, String> params) {
public T form(Map<String, String> params) {
return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded");
}
@SafeVarargs
public final HttpPostRequest form(Pair<String, String>... params) {
public final T form(Pair<String, String>... params) {
return form(mapOf(params));
}
public HttpPostRequest string(String payload, String contentType) {
public T string(String payload, String contentType) {
bytes = payload.getBytes(UTF_8);
header("Content-Length", "" + bytes.length);
header("Content-Length", String.valueOf(bytes.length));
contentType(contentType + "; charset=utf-8");
return this;
return (T) this;
}
@Override
public String getString() throws IOException {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con.setDoOutput(true);
try (OutputStream os = con.getOutputStream()) {
os.write(bytes);
}
URL url = new URL(this.url);
if (con.getResponseCode() / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
try {
throw new ResponseCodeException(url.toString(), con.getResponseCode(), NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new ResponseCodeException(url.toString(), con.getResponseCode(), e);
}
if (bytes != null) {
try (OutputStream os = con.getOutputStream()) {
os.write(bytes);
}
}
checkResponseCode(con);
return NetworkUtils.readFullyAsString(con);
}, retryTimes);
}
}
public static final class HttpPostRequest extends HttpEntityRequest<HttpPostRequest> {
private HttpPostRequest(String url) { super(url, "POST"); }
}
public static final class HttpPutRequest extends HttpEntityRequest<HttpPutRequest> {
private HttpPutRequest(String url) { super(url, "PUT"); }
}
public static HttpGetRequest GET(String url) {
return new HttpGetRequest(url);
}
@@ -216,6 +243,19 @@ public abstract class HttpRequest {
return new HttpPostRequest(url);
}
public static HttpPutRequest PUT(String url) throws MalformedURLException {
return new HttpPutRequest(url);
}
public static HttpDeleteRequest DELETE(String url) {
return new HttpDeleteRequest(url);
}
@SafeVarargs
public static HttpDeleteRequest DELETE(String url, Pair<String, String>... query) {
return DELETE(NetworkUtils.withQuery(url, mapOf(query)));
}
private static String getStringWithRetry(ExceptionalSupplier<String, IOException> supplier, int retryTimes) throws IOException {
Throwable exception = null;
for (int i = 0; i < retryTimes; i++) {
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests for installer state stored in [SettingsMap].
@NotNullByDefault
public final class SettingsMapTest {
/// Tests that vanilla game selection alone is not treated as a modded installation.
@Test
public void minecraftSelectionIsNotModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
assertFalse(settings.isInstallingModdedVersion());
}
/// Tests that selecting a non-vanilla installer is treated as a modded installation.
@Test
public void modLoaderSelectionIsModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
settings.put(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), remoteVersion("fabric"));
assertTrue(settings.isInstallingModdedVersion());
}
/// Creates a minimal remote version for installer state tests.
private static RemoteVersion remoteVersion(String libraryId) {
return new RemoteVersion(libraryId, "1.21.11", "test", Instant.EPOCH, List.of());
}
}
+1
View File
@@ -4,6 +4,7 @@ import org.jackhuang.hmcl.gradle.l10n.ParseLanguageSubtagRegistry
plugins {
id("checkstyle")
id("org.glavo.gradle-wrapper-neo") version "0.1.0"
}
group = "org.jackhuang"
@@ -50,6 +50,7 @@
"themeColor",
"themeColorStyle",
"titleBarTransparent",
"windowTransparent",
"background",
"backgroundOpacity"
]
@@ -137,6 +138,9 @@
"launcherFontFamily": {
"type": "string"
},
"windowTransparent": {
"type": "boolean"
},
"animationDisabled": {
"type": "boolean"
},
+11
View File
@@ -174,6 +174,9 @@
"titleBar": {
"$ref": "#/$defs/titleBar"
},
"windowTransparent": {
"type": "boolean"
},
"overrides": {
"type": "array",
"items": {
@@ -217,6 +220,11 @@
"required": [
"titleBar"
]
},
{
"required": [
"windowTransparent"
]
}
],
"properties": {
@@ -240,6 +248,9 @@
},
"titleBar": {
"$ref": "#/$defs/titleBar"
},
"windowTransparent": {
"type": "boolean"
}
}
},
+1 -1
View File
@@ -17,7 +17,7 @@ fxsvgimage = "1.3"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.7"
authlib-injector = "1.2.8"
lwjgl-unsafe-agent = "2.0"
monet-fx = "0.4.0"
terracotta = "0.4.2"
File diff suppressed because it is too large. Load diff
Binary file not shown.
+5 -3
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=120000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
networkTimeout=10000
retries=0
retryBackOffMs=500
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+109 -16
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# Gradle Wrapper Neo startup script for POSIX.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -56,29 +56,29 @@
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
# (3) This file is maintained by the Gradle Wrapper Neo project. It compiles
# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper
# without storing a Wrapper JAR in the project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
#
##############################################################################
# Attempt to set APP_HOME
# Determine the directory containing this launcher
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
LAUNCHER_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
*) app_path=$LAUNCHER_HOME$link ;;
esac
done
@@ -86,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
LAUNCHER_HOME=$( cd -P "${LAUNCHER_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -170,7 +170,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
LAUNCHER_HOME=$( cygpath --path --mixed "$LAUNCHER_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -202,17 +202,110 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Select the wrapper entry point. GradleWrapperNeo.java is compiled to a temporary
# classes directory only for the first bootstrap; Java packages the final JAR.
LAUNCHER_WRAPPER_DIR=$LAUNCHER_HOME/gradle/wrapper
APP_HOME=
if [ -f "$LAUNCHER_WRAPPER_DIR/gradle-wrapper.properties" ] ; then
APP_HOME=$LAUNCHER_HOME
else
NEO_SEARCH_START=$( pwd -P )
NEO_SEARCH_DIR=$NEO_SEARCH_START
while [ -z "$APP_HOME" ] ; do
NEO_CANDIDATE_WRAPPER_DIR=$NEO_SEARCH_DIR/gradle/wrapper
if [ -f "$NEO_CANDIDATE_WRAPPER_DIR/gradle-wrapper.properties" ] ; then
APP_HOME=$NEO_SEARCH_DIR
elif [ "$NEO_SEARCH_DIR" = / ] ; then
die "ERROR: Could not find gradle/wrapper/gradle-wrapper.properties searching from $NEO_SEARCH_START."
else
NEO_SEARCH_DIR=${NEO_SEARCH_DIR%/*}
if [ -z "$NEO_SEARCH_DIR" ] ; then
NEO_SEARCH_DIR=/
fi
fi
done
fi
PROJECT_NEO_SOURCE=$APP_HOME/gradle/wrapper/GradleWrapperNeo.java
if [ -f "$PROJECT_NEO_SOURCE" ] ; then
NEO_SOURCE=$PROJECT_NEO_SOURCE
else
NEO_SOURCE=$LAUNCHER_WRAPPER_DIR/GradleWrapperNeo.java
fi
NEO_WORK_DIR=$APP_HOME/.gradle/wrapper-neo
NEO_JAR=$NEO_WORK_DIR/gradle-wrapper-neo.jar
NEO_BOOTSTRAP_DIR=$NEO_WORK_DIR/bootstrap/$$
NEO_CLASSES_DIR=$NEO_BOOTSTRAP_DIR/classes
NEO_JAVA_APP_HOME=$APP_HOME
NEO_JAVA_SOURCE=$NEO_SOURCE
NEO_JAVA_JAR=$NEO_JAR
NEO_JAVA_CLASSES_DIR=$NEO_CLASSES_DIR
if "$cygwin" || "$msys" ; then
NEO_JAVA_APP_HOME=$( cygpath --mixed "$APP_HOME" )
NEO_JAVA_SOURCE=$( cygpath --mixed "$NEO_SOURCE" )
NEO_JAVA_JAR=$( cygpath --mixed "$NEO_JAR" )
NEO_JAVA_CLASSES_DIR=$( cygpath --mixed "$NEO_CLASSES_DIR" )
fi
if [ -f "$NEO_SOURCE" ] ; then
if [ -n "$NEO_JAR" ] && [ -f "$NEO_JAR" ] ; then
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
"-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \
"-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \
"-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \
-jar "$NEO_JAVA_JAR" \
"$@"
else
if [ -n "$JAVA_HOME" ] ; then
JAVACMD_COMPILER=$JAVA_HOME/bin/javac
else
JAVACMD_COMPILER=javac
fi
if ! command -v "$JAVACMD_COMPILER" >/dev/null 2>&1
then
die "ERROR: GradleWrapperNeo.java exists, but javac could not be found.
Please run this wrapper with a JDK so GradleWrapperNeo.java can be compiled."
fi
rm -rf "$NEO_CLASSES_DIR"
mkdir -p "$NEO_CLASSES_DIR" || exit
JAVAC_VERSION=$("$JAVACMD_COMPILER" -version 2>&1)
case "$JAVAC_VERSION" in
javac\ 1.*) JAVAC_TARGET_ARGS="-source 8 -target 8" ;;
*) JAVAC_TARGET_ARGS="--release 8" ;;
esac
# shellcheck disable=SC2086
if ! "$JAVACMD_COMPILER" $JAVAC_TARGET_ARGS -Xlint:-options -encoding UTF-8 -d "$NEO_CLASSES_DIR" "$NEO_SOURCE"
then
rm -rf "$NEO_CLASSES_DIR"
die "ERROR: Could not compile $NEO_SOURCE"
fi
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
"-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \
"-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \
"-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \
"-Dgradle.wrapper.neo.bootstrap=true" \
-cp "$NEO_JAVA_CLASSES_DIR" \
GradleWrapperNeo \
"$@"
fi
else
die "ERROR: $NEO_SOURCE was not found."
fi
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
Vendored
+24 -59
View File
@@ -19,75 +19,40 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem Gradle Wrapper Neo forwarding script for Windows
@rem
@rem This file is maintained by the Gradle Wrapper Neo project. It delegates
@rem to gradlew.ps1, which compiles GradleWrapperNeo.java on demand and
@rem launches the Wrapper without storing a Wrapper JAR in the project.
@rem
@rem Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
setlocal EnableExtensions DisableDelayedExpansion
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
set "POWERSHELL_EXE=pwsh.exe"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
set "POWERSHELL_EXE=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if exist "%POWERSHELL_EXE%" goto execute
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
set "POWERSHELL_EXE=powershell.exe"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo ERROR: PowerShell could not be found. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo Please install Windows PowerShell 5.1 or PowerShell 7 and make it available in PATH. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
endlocal
exit /b 1
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
set "GRADLE_WRAPPER_NEO_BATCH_LAUNCH=1"
set "GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS=%*"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0gradlew.ps1"
set "EXIT_CODE=%ERRORLEVEL%"
endlocal & exit /b %EXIT_CODE%
+373
View File
@@ -0,0 +1,373 @@
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle Wrapper Neo startup script for Windows.
#
# This file is maintained by the Gradle Wrapper Neo project. It compiles
# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper
# without storing a Wrapper JAR in the project.
#
# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
#
##############################################################################
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$script:WrapperExitCode = 1
$script:AppBaseName = [IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
$script:LauncherHome = [IO.Path]::GetFullPath($PSScriptRoot)
function ConvertFrom-WindowsCommandLine {
param([string] $CommandLine)
if ($null -eq ('GradleWrapperNeo.NativeCommandLine' -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
namespace GradleWrapperNeo {
public static class NativeCommandLine {
[DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CommandLineToArgvW(string commandLine, out int argumentCount);
[DllImport("kernel32.dll")]
public static extern IntPtr LocalFree(IntPtr memory);
}
}
"@
}
$nativeCommandLine = 'gradlew.exe ' + $CommandLine
$argumentCount = 0
$argumentVector = [GradleWrapperNeo.NativeCommandLine]::CommandLineToArgvW(
$nativeCommandLine,
[ref] $argumentCount
)
if ($argumentVector -eq [IntPtr]::Zero) {
throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error())
}
try {
$result = New-Object 'System.Collections.Generic.List[string]'
for ($index = 1; $index -lt $argumentCount; $index++) {
$argumentPointer = [Runtime.InteropServices.Marshal]::ReadIntPtr(
$argumentVector,
$index * [IntPtr]::Size
)
[void] $result.Add([Runtime.InteropServices.Marshal]::PtrToStringUni($argumentPointer))
}
return $result.ToArray()
} finally {
[void] [GradleWrapperNeo.NativeCommandLine]::LocalFree($argumentVector)
}
}
function Split-ArgumentString {
param([string] $Value)
if ($null -eq $Value) {
return @()
}
$result = New-Object 'System.Collections.Generic.List[string]'
$currentOption = New-Object Text.StringBuilder
[char] $currentQuote = [char] 0
$insideQuote = $false
$hasOption = $false
for ($index = 0; $index -lt $Value.Length; $index++) {
[char] $character = $Value[$index]
if ((-not $insideQuote) -and [char]::IsWhiteSpace($character)) {
if ($hasOption) {
[void] $result.Add($currentOption.ToString())
[void] $currentOption.Clear()
$hasOption = $false
}
} elseif ((-not $insideQuote) -and (($character -eq [char] 34) -or ($character -eq [char] 39))) {
$currentQuote = $character
$insideQuote = $true
$hasOption = $true
} elseif ($insideQuote -and ($character -eq $currentQuote)) {
$insideQuote = $false
} else {
[void] $currentOption.Append($character)
$hasOption = $true
}
}
if ($hasOption) {
[void] $result.Add($currentOption.ToString())
}
return $result.ToArray()
}
function Resolve-JavaExecutable {
if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) {
$javaHome = $env:JAVA_HOME.Trim([char] 34)
$javaExecutable = Join-Path $javaHome 'bin\java.exe'
if (-not (Test-Path -LiteralPath $javaExecutable -PathType Leaf)) {
throw "JAVA_HOME is set to an invalid directory: $javaHome"
}
return [IO.Path]::GetFullPath($javaExecutable)
}
$javaCommand = Get-Command -Name 'java.exe' -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -eq $javaCommand) {
throw "JAVA_HOME is not set and no 'java' command could be found in PATH."
}
return $javaCommand.Path
}
function Resolve-JavacExecutable {
if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) {
$javaHome = $env:JAVA_HOME.Trim([char] 34)
$javacExecutable = Join-Path $javaHome 'bin\javac.exe'
if (Test-Path -LiteralPath $javacExecutable -PathType Leaf) {
return [IO.Path]::GetFullPath($javacExecutable)
}
} else {
$javacCommand = Get-Command -Name 'javac.exe' -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -ne $javacCommand) {
return $javacCommand.Path
}
}
throw 'GradleWrapperNeo.java exists, but javac could not be found. Run this wrapper with a JDK so the source can be compiled.'
}
function Find-AppHome {
param([string] $LauncherHome)
$launcherProperties = Join-Path $LauncherHome 'gradle\wrapper\gradle-wrapper.properties'
if (Test-Path -LiteralPath $launcherProperties -PathType Leaf) {
return $LauncherHome
}
$currentLocation = Get-Location
if ($currentLocation.Provider.Name -ne 'FileSystem') {
throw "The current location is not a file system directory: $currentLocation"
}
$searchDirectory = Get-Item -LiteralPath $currentLocation.Path
$searchStart = $searchDirectory.FullName
while ($null -ne $searchDirectory) {
$propertiesFile = Join-Path $searchDirectory.FullName 'gradle\wrapper\gradle-wrapper.properties'
if (Test-Path -LiteralPath $propertiesFile -PathType Leaf) {
return $searchDirectory.FullName
}
$searchDirectory = $searchDirectory.Parent
}
throw "Could not find gradle/wrapper/gradle-wrapper.properties searching from '$searchStart'."
}
function ConvertTo-JavaPath {
param([string] $Path)
return $Path.Replace('\', '/')
}
function ConvertTo-WindowsCommandLineArgument {
param([AllowEmptyString()][string] $Argument)
if (($Argument.Length -gt 0) -and ($Argument -notmatch '[\s"]')) {
return $Argument
}
$result = New-Object Text.StringBuilder
[void] $result.Append([char] 34)
$backslashCount = 0
for ($index = 0; $index -lt $Argument.Length; $index++) {
[char] $character = $Argument[$index]
if ($character -eq [char] 92) {
$backslashCount++
continue
}
if ($character -eq [char] 34) {
[void] $result.Append(([string] [char] 92) * (($backslashCount * 2) + 1))
} elseif ($backslashCount -gt 0) {
[void] $result.Append(([string] [char] 92) * $backslashCount)
}
$backslashCount = 0
[void] $result.Append($character)
}
if ($backslashCount -gt 0) {
[void] $result.Append(([string] [char] 92) * ($backslashCount * 2))
}
[void] $result.Append([char] 34)
return $result.ToString()
}
function Invoke-NativeApplication {
param(
[string] $Executable,
[string[]] $Arguments
)
$quotedArguments = New-Object 'System.Collections.Generic.List[string]'
foreach ($argument in $Arguments) {
[void] $quotedArguments.Add((ConvertTo-WindowsCommandLineArgument -Argument $argument))
}
$startInfo = New-Object Diagnostics.ProcessStartInfo
$startInfo.FileName = $Executable
$startInfo.Arguments = [string]::Join(' ', $quotedArguments.ToArray())
$startInfo.UseShellExecute = $false
$process = New-Object Diagnostics.Process
$process.StartInfo = $startInfo
try {
if (-not $process.Start()) {
throw "Could not start '$Executable'."
}
$process.WaitForExit()
return $process.ExitCode
} finally {
$process.Dispose()
}
}
function Compile-WrapperSource {
param(
[string] $SourceFile,
[string] $ClassesDirectory,
[string] $BootstrapDirectory
)
$javacExecutable = Resolve-JavacExecutable
[void] [IO.Directory]::CreateDirectory($ClassesDirectory)
$versionOutput = & $javacExecutable -version 2>&1
$versionExitCode = $LASTEXITCODE
if ($versionExitCode -ne 0) {
Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue
throw "Could not run javac at '$javacExecutable'."
}
$versionText = ($versionOutput | Out-String).Trim()
if ($versionText.StartsWith('javac 1.')) {
[string[]] $targetArguments = @('-source', '8', '-target', '8')
} else {
[string[]] $targetArguments = @('--release', '8')
}
[string[]] $compileArguments = @(
$targetArguments
'-Xlint:-options'
'-encoding'
'UTF-8'
'-d'
$ClassesDirectory
$SourceFile
)
& $javacExecutable @compileArguments
$compileExitCode = $LASTEXITCODE
if ($compileExitCode -ne 0) {
Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue
throw "Could not compile '$SourceFile'."
}
}
function Invoke-GradleWrapper {
param([string[]] $GradleArguments)
$appHome = Find-AppHome -LauncherHome $script:LauncherHome
$launcherWrapperDirectory = Join-Path $script:LauncherHome 'gradle\wrapper'
$projectSource = Join-Path $appHome 'gradle\wrapper\GradleWrapperNeo.java'
if (Test-Path -LiteralPath $projectSource -PathType Leaf) {
$sourceFile = $projectSource
} else {
$sourceFile = Join-Path $launcherWrapperDirectory 'GradleWrapperNeo.java'
}
if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) {
throw "Wrapper source file '$sourceFile' was not found."
}
$workDirectory = Join-Path $appHome '.gradle\wrapper-neo'
$jarFile = Join-Path $workDirectory 'gradle-wrapper-neo.jar'
$bootstrapDirectory = Join-Path (Join-Path $workDirectory 'bootstrap') ([Guid]::NewGuid().ToString('N'))
$classesDirectory = Join-Path $bootstrapDirectory 'classes'
$javaExecutable = Resolve-JavaExecutable
[string[]] $jvmArguments = @('-Xmx64m', '-Xms64m')
foreach ($optionSource in @($env:JAVA_OPTS, $env:GRADLE_OPTS)) {
foreach ($option in @(Split-ArgumentString -Value $optionSource)) {
$jvmArguments += $option
}
}
$javaAppHome = ConvertTo-JavaPath -Path $appHome
$javaSourceFile = ConvertTo-JavaPath -Path $sourceFile
$javaJarFile = ConvertTo-JavaPath -Path $jarFile
$javaClassesDirectory = ConvertTo-JavaPath -Path $classesDirectory
[string[]] $javaArguments = @(
$jvmArguments
"-Dorg.gradle.appname=$script:AppBaseName"
"-Dorg.gradle.wrapper.neo.app-home=$javaAppHome"
"-Dorg.gradle.wrapper.neo.source-file=$javaSourceFile"
"-Dorg.gradle.wrapper.neo.jar-file=$javaJarFile"
)
$cachedJar = Get-Item -LiteralPath $jarFile -ErrorAction SilentlyContinue
if (($null -ne $cachedJar) -and ($cachedJar.Length -eq 0)) {
Remove-Item -LiteralPath $jarFile -Force -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $jarFile -PathType Leaf) {
$javaArguments += @('-jar', $javaJarFile)
} else {
Compile-WrapperSource -SourceFile $sourceFile -ClassesDirectory $classesDirectory -BootstrapDirectory $bootstrapDirectory
$javaArguments += @(
'-Dgradle.wrapper.neo.bootstrap=true'
'-cp'
$javaClassesDirectory
'GradleWrapperNeo'
)
}
$javaArguments += $GradleArguments
$script:WrapperExitCode = Invoke-NativeApplication -Executable $javaExecutable -Arguments $javaArguments
}
try {
if ($env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -eq '1') {
$rawArguments = $env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS
Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -ErrorAction SilentlyContinue
Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS -ErrorAction SilentlyContinue
[string[]] $gradleArguments = @(ConvertFrom-WindowsCommandLine -CommandLine $rawArguments)
} else {
[string[]] $gradleArguments = @($args)
}
Invoke-GradleWrapper -GradleArguments $gradleArguments
exit $script:WrapperExitCode
} catch {
[Console]::Error.WriteLine()
[Console]::Error.WriteLine('ERROR: ' + $_.Exception.Message)
[Console]::Error.WriteLine()
exit 1
}