支持纯色背景 (#4184)

This commit is contained in:
Glavo
2025-08-04 15:17:46 +08:00
committed by GitHub
parent 9a27c27b9a
commit b62659de62
10 changed files with 226 additions and 7 deletions

View File

@@ -29,6 +29,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.ObservableMap; import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet; import javafx.collections.ObservableSet;
import javafx.scene.paint.Paint;
import org.hildan.fxgson.creators.ObservableListCreator; import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator; import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator; 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.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer; import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter; 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;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -60,6 +62,7 @@ public final class Config implements Observable {
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true)) .registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType .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(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.registerTypeAdapter(Paint.class, new PaintAdapter())
.setPrettyPrinting() .setPrettyPrinting()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create(); .create();
@@ -87,6 +90,9 @@ public final class Config implements Observable {
@SerializedName("bgurl") @SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty(); private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("bgpaint")
private ObjectProperty<Paint> backgroundPaint = new SimpleObjectProperty<>();
@SerializedName("commonDirType") @SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT); private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@@ -172,7 +178,7 @@ public final class Config implements Observable {
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false); private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@SerializedName("authlibInjectorServers") @SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server }); private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server});
@SerializedName("addedLittleSkin") @SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false); private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@@ -274,6 +280,18 @@ public final class Config implements Observable {
this.backgroundImageUrl.set(backgroundImageUrl); this.backgroundImageUrl.set(backgroundImageUrl);
} }
public Paint getBackgroundPaint() {
return backgroundPaint.get();
}
public ObjectProperty<Paint> backgroundPaintProperty() {
return backgroundPaint;
}
public void setBackgroundPaint(Paint backgroundPaint) {
this.backgroundPaint.set(backgroundPaint);
}
public EnumCommonDirectory getCommonDirType() { public EnumCommonDirectory getCommonDirType() {
return commonDirType.get(); return commonDirType.get();
} }

View File

@@ -22,5 +22,6 @@ public enum EnumBackgroundImage {
CUSTOM, CUSTOM,
CLASSIC, CLASSIC,
NETWORK, NETWORK,
TRANSLUCENT TRANSLUCENT,
PAINT
} }

View File

@@ -47,6 +47,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
@@ -745,6 +746,106 @@ public final class FXUtils {
property.removeListener(binding); property.removeListener(binding);
} }
private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener {
private final WeakReference<ColorPicker> colorPickerRef;
private final WeakReference<Property<Paint>> propertyRef;
private final int hashCode;
private boolean updating = false;
private PaintBidirectionalBinding(ColorPicker colorPicker, Property<Paint> 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<Paint> 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<Paint> 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<Paint> 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<Paint> property) {
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
colorPicker.valueProperty().removeListener(binding);
property.removeListener(binding);
}
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) { public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
int itemCount = children.length; int itemCount = children.length;
int childSelectedCount = 0; int childSelectedCount = 0;

View File

@@ -24,11 +24,13 @@ import javafx.beans.property.*;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Paint;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
@@ -297,4 +299,35 @@ public final class MultiFileItem<T> extends VBox {
return pane; return pane;
} }
} }
public static final class PaintOption<T> extends Option<T> {
private final ColorPicker colorPicker = new ColorPicker();
public PaintOption(String title, T data) {
super(title, data);
}
public PaintOption<T> bindBidirectional(Property<Paint> 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;
}
}
} }

View File

@@ -61,10 +61,7 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.*;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -135,6 +132,7 @@ public class DecoratorController {
config().backgroundImageTypeProperty().addListener(weakListener); config().backgroundImageTypeProperty().addListener(weakListener);
config().backgroundImageProperty().addListener(weakListener); config().backgroundImageProperty().addListener(weakListener);
config().backgroundImageUrlProperty().addListener(weakListener); config().backgroundImageUrlProperty().addListener(weakListener);
config().backgroundPaintProperty().addListener(weakListener);
// pass key events to current dialog / current page // pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> { decorator.addEventFilter(KeyEvent.ANY, e -> {
@@ -224,6 +222,8 @@ public class DecoratorController {
break; break;
case TRANSLUCENT: case TRANSLUCENT:
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY)); 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) { if (image == null) {
image = loadDefaultBackgroundImage(); image = loadDefaultBackgroundImage();

View File

@@ -119,7 +119,9 @@ public class PersonalizationPage extends StackPane {
.bindBidirectional(config().backgroundImageProperty()), .bindBidirectional(config().backgroundImageProperty()),
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK) new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
.setValidators(new URLValidator(true)) .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()); backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundSublist.subtitleProperty().bind( backgroundSublist.subtitleProperty().bind(

View File

@@ -812,6 +812,7 @@ launcher.background.classic=Classic
launcher.background.default=Default launcher.background.default=Default
launcher.background.default.tooltip=Or "background.png/.jpg/.gif/.webp" and the images in the "bg" directory launcher.background.default.tooltip=Or "background.png/.jpg/.gif/.webp" and the images in the "bg" directory
launcher.background.network=From URL launcher.background.network=From URL
launcher.background.paint=Solid Color
launcher.background.translucent=Translucent launcher.background.translucent=Translucent
launcher.cache_directory=Cache Directory launcher.cache_directory=Cache Directory
launcher.cache_directory.clean=Clear Cache launcher.cache_directory.clean=Clear Cache

View File

@@ -628,6 +628,7 @@ launcher.background.classic=經典
launcher.background.default=預設 launcher.background.default=預設
launcher.background.default.tooltip=自動尋找啟動器同目錄下的「background.png/.jpg/.gif/.webp」及「bg」目錄內的圖片 launcher.background.default.tooltip=自動尋找啟動器同目錄下的「background.png/.jpg/.gif/.webp」及「bg」目錄內的圖片
launcher.background.network=網路 launcher.background.network=網路
launcher.background.paint=純色
launcher.background.translucent=半透明 launcher.background.translucent=半透明
launcher.cache_directory=檔案下載快取目錄 launcher.cache_directory=檔案下載快取目錄
launcher.cache_directory.clean=清理 launcher.cache_directory.clean=清理

View File

@@ -638,6 +638,7 @@ launcher.background.classic=经典
launcher.background.default=默认 launcher.background.default=默认
launcher.background.default.tooltip=自动检索启动器同文件夹下的“background.png/.jpg/.gif/.webp”及“bg”文件夹内的图片 launcher.background.default.tooltip=自动检索启动器同文件夹下的“background.png/.jpg/.gif/.webp”及“bg”文件夹内的图片
launcher.background.network=网络 launcher.background.network=网络
launcher.background.paint=纯色
launcher.background.translucent=半透明 launcher.background.translucent=半透明
launcher.cache_directory=文件下载缓存文件夹 launcher.cache_directory=文件下载缓存文件夹
launcher.cache_directory.clean=清理缓存 launcher.cache_directory.clean=清理缓存

View File

@@ -0,0 +1,61 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.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<Paint> {
@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);
}
}