支持修改主页背景的不透明度 (#3205)

Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
吞云(TuYw)
2025-08-04 16:05:47 +08:00
committed by GitHub
parent b62659de62
commit 4bc6ffa0f0
9 changed files with 151 additions and 25 deletions

View File

@@ -90,6 +90,9 @@ public final class Config implements Observable {
@SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("bgImageOpacity")
private IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100);
@SerializedName("bgpaint")
private ObjectProperty<Paint> backgroundPaint = new SimpleObjectProperty<>();
@@ -292,6 +295,18 @@ public final class Config implements Observable {
this.backgroundPaint.set(backgroundPaint);
}
public int getBackgroundImageOpacity() {
return backgroundImageOpacity.get();
}
public void setBackgroundImageOpacity(int backgroundImageOpacity) {
this.backgroundImageOpacity.set(backgroundImageOpacity);
}
public IntegerProperty backgroundImageOpacityProperty() {
return backgroundImageOpacity;
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}

View File

@@ -31,12 +31,16 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
@@ -44,6 +48,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
@@ -55,6 +60,7 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -62,15 +68,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
@@ -120,19 +125,13 @@ public class DecoratorController {
// Setup background
decorator.setContentBackground(getBackground());
changeBackgroundListener = o -> {
final int currentCount = ++this.changeBackgroundCount;
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
.thenAcceptAsync(background -> {
if (this.changeBackgroundCount == currentCount)
decorator.setContentBackground(background);
}, Schedulers.javafx());
};
changeBackgroundListener = o -> updateBackground();
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
config().backgroundImageTypeProperty().addListener(weakListener);
config().backgroundImageProperty().addListener(weakListener);
config().backgroundImageUrlProperty().addListener(weakListener);
config().backgroundPaintProperty().addListener(weakListener);
config().backgroundImageOpacityProperty().addListener(weakListener);
// pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> {
@@ -193,6 +192,20 @@ public class DecoratorController {
@SuppressWarnings("FieldCanBeLocal") // Strong reference
private final InvalidationListener changeBackgroundListener;
private void updateBackground() {
final int currentCount = ++this.changeBackgroundCount;
Task.supplyAsync(Schedulers.io(), this::getBackground)
.setName("Update background")
.whenComplete(Schedulers.javafx(), (background, exception) -> {
if (exception == null) {
if (this.changeBackgroundCount == currentCount)
decorator.setContentBackground(background);
} else {
LOG.warning("Failed to update background", exception);
}
}).start();
}
private Background getBackground() {
EnumBackgroundImage imageType = config().getBackgroundImageType();
@@ -221,14 +234,59 @@ public class DecoratorController {
image = newBuiltinImage("/assets/img/background-classic.jpg");
break;
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, Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.), CornerRadii.EMPTY, Insets.EMPTY));
case PAINT:
return new Background(new BackgroundFill(Objects.requireNonNullElse(config().getBackgroundPaint(), Color.WHITE), CornerRadii.EMPTY, Insets.EMPTY));
Paint paint = config().getBackgroundPaint();
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null)
color = Color.WHITE; // Default to white if no color is set
if (opacity < 1.)
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), opacity);
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
} else {
// TODO: Support opacity for non-color paints
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
}
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
return createBackgroundWithOpacity(image, config().getBackgroundImageOpacity());
}
private Background createBackgroundWithOpacity(Image image, int opacity) {
if (opacity <= 0) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
} else if (opacity >= 100) {
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());
PixelReader pixelReader = image.getPixelReader();
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 / 100);
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)
));
}
}
/**

View File

@@ -19,21 +19,22 @@ package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.binding.When;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
@@ -56,6 +57,20 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class PersonalizationPage extends StackPane {
private static int snapOpacity(double val) {
if (val <= 0) {
return 0;
} else if (Double.isNaN(val) || val >= 100.) {
return 100;
}
int prevTick = (int) (val / 5);
int prevTickValue = prevTick * 5;
int nextTickValue = (prevTick + 1) * 5;
return (val - prevTickValue) > (nextTickValue - val) ? nextTickValue : prevTickValue;
}
public PersonalizationPage() {
VBox content = new VBox(10);
content.setPadding(new Insets(10));
@@ -112,7 +127,6 @@ public class PersonalizationPage extends StackPane {
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
.setTooltip(i18n("launcher.background.default.tooltip")),
new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC),
new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
.setChooserTitle(i18n("launcher.background.choose"))
.addExtensionFilter(FXUtils.getImageExtensionFilter())
@@ -129,7 +143,49 @@ public class PersonalizationPage extends StackPane {
.then(i18n("launcher.background.default"))
.otherwise(config().backgroundImageProperty()));
componentList.getContent().add(backgroundItem);
HBox opacityItem = new HBox(8);
{
opacityItem.setAlignment(Pos.CENTER);
Label label = new Label(i18n("settings.launcher.background.settings.opacity"));
JFXSlider slider = new JFXSlider(0, 100,
config().getBackgroundImageType() != EnumBackgroundImage.TRANSLUCENT
? config().getBackgroundImageOpacity() : 50);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(10);
slider.setMinorTickCount(1);
slider.setBlockIncrement(5);
slider.setSnapToTicks(true);
HBox.setHgrow(slider, Priority.ALWAYS);
if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
slider.setDisable(true);
config().backgroundImageTypeProperty().addListener(new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends EnumBackgroundImage> observable, EnumBackgroundImage oldValue, EnumBackgroundImage newValue) {
if (newValue != EnumBackgroundImage.TRANSLUCENT) {
config().backgroundImageTypeProperty().removeListener(this);
slider.setDisable(false);
slider.setValue(100);
}
}
});
}
Label textOpacity = new Label();
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
slider.setValueFactory(s -> valueBinding);
slider.valueProperty().addListener((observable, oldValue, newValue) ->
config().setBackgroundImageOpacity(snapOpacity(newValue.doubleValue())));
opacityItem.getChildren().setAll(label, slider, textOpacity);
}
componentList.getContent().setAll(backgroundItem, opacityItem);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
}

View File

@@ -813,7 +813,6 @@ 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
launcher.cache_directory.choose=Choose cache directory
@@ -1354,6 +1353,7 @@ settings.launcher.theme=Theme
settings.launcher.title_transparent=Transparent Titlebar
settings.launcher.turn_off_animations=Disable Animation (Applies After Restart)
settings.launcher.version_list_source=Version List
settings.launcher.background.settings.opacity=Opacity
settings.memory=Memory
settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated

View File

@@ -817,7 +817,6 @@ launcher.background.classic=Clásico
launcher.background.default=Por defecto
launcher.background.default.tooltip=O «background.png/.jpg/.gif/.webp» y las imágenes en el directorio «bg».
launcher.background.network=Desde la URL
launcher.background.translucent=Translúcido
launcher.cache_directory=Directorio de la caché
launcher.cache_directory.clean=Borrar caché
launcher.cache_directory.choose=Elegir el directorio de la caché

View File

@@ -540,7 +540,6 @@ launcher.background.choose=背景画像ファイルを選択してください
launcher.background.default=標準
launcher.background.default.tooltip=ランチャーと同じディレクトリにある background.png/.jpg/.gif/.webp と bg フォルダから自動的に画像を取得します。
launcher.background.network=ネットワーク
launcher.background.translucent=半透明
launcher.cache_directory=キャッシュ用のディレクトリ
launcher.cache_directory.clean=クリアランス
launcher.cache_directory.choose=キャッシュするディレクトリを選択します

View File

@@ -816,7 +816,6 @@ launcher.background.classic=Классическое
launcher.background.default=По умолчанию
launcher.background.default.tooltip=Или «background.png/.jpg/.gif/.webp» и изображения в папке «bg».
launcher.background.network=По ссылке
launcher.background.translucent=Полупрозрачный
launcher.cache_directory=Папка с кэшем
launcher.cache_directory.clean=Очистить кэш
launcher.cache_directory.choose=Выберите папку для кэша

View File

@@ -629,7 +629,6 @@ 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=清理
launcher.cache_directory.choose=選取檔案下載快取目錄
@@ -1151,6 +1150,7 @@ settings.launcher.theme=主題
settings.launcher.title_transparent=標題欄透明
settings.launcher.turn_off_animations=關閉動畫 (重啟後生效)
settings.launcher.version_list_source=版本清單來源
settings.launcher.background.settings.opacity=不透明度
settings.memory=遊戲記憶體
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB

View File

@@ -639,7 +639,6 @@ 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=清理缓存
launcher.cache_directory.choose=选择文件下载缓存文件夹
@@ -1161,6 +1160,7 @@ settings.launcher.theme=主题
settings.launcher.title_transparent=标题栏透明
settings.launcher.turn_off_animations=关闭动画 (重启后生效)
settings.launcher.version_list_source=版本列表源
settings.launcher.background.settings.opacity=不透明度
settings.memory=游戏内存
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB