From b62659de62fc0fe5a2eb7d8928f23a733222ba5e Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 4 Aug 2025 15:17:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BA=AF=E8=89=B2=E8=83=8C?= =?UTF-8?q?=E6=99=AF=20(#4184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/setting/Config.java | 20 +++- .../hmcl/setting/EnumBackgroundImage.java | 3 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 101 ++++++++++++++++++ .../hmcl/ui/construct/MultiFileItem.java | 33 ++++++ .../ui/decorator/DecoratorController.java | 8 +- .../hmcl/ui/main/PersonalizationPage.java | 4 +- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/util/gson/PaintAdapter.java | 61 +++++++++++ 10 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PaintAdapter.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 39630b36c..996988d2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -29,6 +29,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.collections.ObservableSet; +import javafx.scene.paint.Paint; import org.hildan.fxgson.creators.ObservableListCreator; import org.hildan.fxgson.creators.ObservableMapCreator; import org.hildan.fxgson.creators.ObservableSetCreator; @@ -37,6 +38,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer; import org.jackhuang.hmcl.util.gson.FileTypeAdapter; +import org.jackhuang.hmcl.util.gson.PaintAdapter; import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; import org.jackhuang.hmcl.util.javafx.ObservableHelper; @@ -60,6 +62,7 @@ public final class Config implements Observable { .registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true)) .registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType .registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy + .registerTypeAdapter(Paint.class, new PaintAdapter()) .setPrettyPrinting() .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .create(); @@ -87,6 +90,9 @@ public final class Config implements Observable { @SerializedName("bgurl") private StringProperty backgroundImageUrl = new SimpleStringProperty(); + @SerializedName("bgpaint") + private ObjectProperty backgroundPaint = new SimpleObjectProperty<>(); + @SerializedName("commonDirType") private ObjectProperty commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT); @@ -172,7 +178,7 @@ public final class Config implements Observable { private BooleanProperty titleTransparent = new SimpleBooleanProperty(false); @SerializedName("authlibInjectorServers") - private ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server }); + private ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server}); @SerializedName("addedLittleSkin") private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false); @@ -274,6 +280,18 @@ public final class Config implements Observable { this.backgroundImageUrl.set(backgroundImageUrl); } + public Paint getBackgroundPaint() { + return backgroundPaint.get(); + } + + public ObjectProperty backgroundPaintProperty() { + return backgroundPaint; + } + + public void setBackgroundPaint(Paint backgroundPaint) { + this.backgroundPaint.set(backgroundPaint); + } + public EnumCommonDirectory getCommonDirType() { return commonDirType.get(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumBackgroundImage.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumBackgroundImage.java index 0192759e1..af9d9d3bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumBackgroundImage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/EnumBackgroundImage.java @@ -22,5 +22,6 @@ public enum EnumBackgroundImage { CUSTOM, CLASSIC, NETWORK, - TRANSLUCENT + TRANSLUCENT, + PAINT } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 16d1ad049..0a9b79781 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -47,6 +47,7 @@ import javafx.scene.image.ImageView; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; @@ -745,6 +746,106 @@ public final class FXUtils { property.removeListener(binding); } + private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener { + private final WeakReference colorPickerRef; + private final WeakReference> propertyRef; + private final int hashCode; + + private boolean updating = false; + + private PaintBidirectionalBinding(ColorPicker colorPicker, Property property) { + this.colorPickerRef = new WeakReference<>(colorPicker); + this.propertyRef = new WeakReference<>(property); + this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final ColorPicker colorPicker = colorPickerRef.get(); + final Property property = propertyRef.get(); + + if (colorPicker == null || property == null) { + if (colorPicker != null) { + colorPicker.valueProperty().removeListener(this); + } + + if (property != null) { + property.removeListener(this); + } + } else { + updating = true; + try { + if (property == sourceProperty) { + Paint newValue = property.getValue(); + if (newValue instanceof Color) + colorPicker.setValue((Color) newValue); + else + colorPicker.setValue(null); + } else { + Paint newValue = colorPicker.getValue(); + property.setValue(newValue); + } + } finally { + updating = false; + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return colorPickerRef.get() == null || propertyRef.get() == null; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof FXUtils.PaintBidirectionalBinding)) + return false; + + var that = (FXUtils.PaintBidirectionalBinding) o; + + final ColorPicker colorPicker = this.colorPickerRef.get(); + final Property property = this.propertyRef.get(); + + final ColorPicker thatColorPicker = that.colorPickerRef.get(); + final Property thatProperty = that.propertyRef.get(); + + if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null) + return false; + + return colorPicker == thatColorPicker && property == thatProperty; + } + } + + public static void bindPaint(ColorPicker colorPicker, Property property) { + PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property); + + colorPicker.valueProperty().removeListener(binding); + property.removeListener(binding); + + if (property.getValue() instanceof Color) + colorPicker.setValue((Color) property.getValue()); + else + colorPicker.setValue(null); + + colorPicker.valueProperty().addListener(binding); + property.addListener(binding); + } + + public static void unbindColorPicker(ColorPicker colorPicker, Property property) { + PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property); + colorPicker.valueProperty().removeListener(binding); + property.removeListener(binding); + } + public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) { int itemCount = children.length; int childSelectedCount = 0; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java index e8235ebec..d20502727 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java @@ -24,11 +24,13 @@ import javafx.beans.property.*; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.ColorPicker; import javafx.scene.control.Label; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Paint; import javafx.stage.FileChooser; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.StringUtils; @@ -297,4 +299,35 @@ public final class MultiFileItem extends VBox { return pane; } } + + public static final class PaintOption extends Option { + private final ColorPicker colorPicker = new ColorPicker(); + + public PaintOption(String title, T data) { + super(title, data); + } + + public PaintOption bindBidirectional(Property property) { + FXUtils.bindPaint(colorPicker, property); + return this; + } + + @Override + protected Node createItem(ToggleGroup group) { + BorderPane pane = new BorderPane(); + pane.setPadding(new Insets(3)); + FXUtils.setLimitHeight(pane, 30); + + left.setText(title); + BorderPane.setAlignment(left, Pos.CENTER_LEFT); + left.setToggleGroup(group); + left.setUserData(data); + pane.setLeft(left); + + colorPicker.disableProperty().bind(left.selectedProperty().not()); + BorderPane.setAlignment(colorPicker, Pos.CENTER_RIGHT); + pane.setRight(colorPicker); + return pane; + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 9b591d2c3..37af645e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -61,10 +61,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.Random; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; @@ -135,6 +132,7 @@ public class DecoratorController { config().backgroundImageTypeProperty().addListener(weakListener); config().backgroundImageProperty().addListener(weakListener); config().backgroundImageUrlProperty().addListener(weakListener); + config().backgroundPaintProperty().addListener(weakListener); // pass key events to current dialog / current page decorator.addEventFilter(KeyEvent.ANY, e -> { @@ -224,6 +222,8 @@ public class DecoratorController { break; case TRANSLUCENT: return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY)); + case PAINT: + return new Background(new BackgroundFill(Objects.requireNonNullElse(config().getBackgroundPaint(), Color.WHITE), CornerRadii.EMPTY, Insets.EMPTY)); } if (image == null) { image = loadDefaultBackgroundImage(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java index b366b07ee..bcb21107a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java @@ -119,7 +119,9 @@ public class PersonalizationPage extends StackPane { .bindBidirectional(config().backgroundImageProperty()), new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK) .setValidators(new URLValidator(true)) - .bindBidirectional(config().backgroundImageUrlProperty()) + .bindBidirectional(config().backgroundImageUrlProperty()), + new MultiFileItem.PaintOption<>(i18n("launcher.background.paint"), EnumBackgroundImage.PAINT) + .bindBidirectional(config().backgroundPaintProperty()) )); backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty()); backgroundSublist.subtitleProperty().bind( diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a237e15b8..a0b9299c9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -812,6 +812,7 @@ launcher.background.classic=Classic launcher.background.default=Default launcher.background.default.tooltip=Or "background.png/.jpg/.gif/.webp" and the images in the "bg" directory launcher.background.network=From URL +launcher.background.paint=Solid Color launcher.background.translucent=Translucent launcher.cache_directory=Cache Directory launcher.cache_directory.clean=Clear Cache diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 90c3b63fd..6b763c004 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -628,6 +628,7 @@ launcher.background.classic=經典 launcher.background.default=預設 launcher.background.default.tooltip=自動尋找啟動器同目錄下的「background.png/.jpg/.gif/.webp」及「bg」目錄內的圖片 launcher.background.network=網路 +launcher.background.paint=純色 launcher.background.translucent=半透明 launcher.cache_directory=檔案下載快取目錄 launcher.cache_directory.clean=清理 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index de1c55bcf..30628a3cd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -638,6 +638,7 @@ launcher.background.classic=经典 launcher.background.default=默认 launcher.background.default.tooltip=自动检索启动器同文件夹下的“background.png/.jpg/.gif/.webp”及“bg”文件夹内的图片 launcher.background.network=网络 +launcher.background.paint=纯色 launcher.background.translucent=半透明 launcher.cache_directory=文件下载缓存文件夹 launcher.cache_directory.clean=清理缓存 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PaintAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PaintAdapter.java new file mode 100644 index 000000000..e771a3d2f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PaintAdapter.java @@ -0,0 +1,61 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.gson; + +import com.google.gson.*; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import javafx.scene.paint.*; + +import java.io.IOException; + +public final class PaintAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Paint value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + if (value instanceof Color) { + Color color = (Color) value; + int red = (int) Math.round(color.getRed() * 255.); + int green = (int) Math.round(color.getGreen() * 255.); + int blue = (int) Math.round(color.getBlue() * 255.); + int opacity = (int) Math.round(color.getOpacity() * 255.); + out.value(String.format("#%02x%02x%02x%02x", red, green, blue, opacity)); + } else if (value instanceof LinearGradient + || value instanceof RadialGradient) { + out.value(value.toString()); + } else { + throw new JsonParseException("Unsupported Paint type: " + value.getClass().getName()); + } + } + + @Override + public Paint read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String value = in.nextString(); + return Paint.valueOf(value); + } +}