From 4bc6ffa0f0bdc5c91cf78141fdb7bc371f1f4c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=9E=E4=BA=91=28TuYw=29?= <131483483+TunYuntuwuQWQ@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:05:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BF=AE=E6=94=B9=E4=B8=BB?= =?UTF-8?q?=E9=A1=B5=E8=83=8C=E6=99=AF=E7=9A=84=E4=B8=8D=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E5=BA=A6=20(#3205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Glavo --- .../org/jackhuang/hmcl/setting/Config.java | 15 ++++ .../ui/decorator/DecoratorController.java | 84 ++++++++++++++++--- .../hmcl/ui/main/PersonalizationPage.java | 68 +++++++++++++-- .../resources/assets/lang/I18N.properties | 2 +- .../resources/assets/lang/I18N_es.properties | 1 - .../resources/assets/lang/I18N_ja.properties | 1 - .../resources/assets/lang/I18N_ru.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 2 +- .../assets/lang/I18N_zh_CN.properties | 2 +- 9 files changed, 151 insertions(+), 25 deletions(-) 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 996988d2a..60afbbee1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -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 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(); } 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 37af645e1..7929aae07 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 @@ -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) + )); + } } /** 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 bcb21107a..bbf5925f4 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 @@ -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 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); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a0b9299c9..d0010892e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 7e66815c2..a6941862a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -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é diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 609bef992..7e5c0f6b1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -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=キャッシュするディレクトリを選択します diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 3b80f79d7..f639a4380 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -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=Выберите папку для кэша diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 6b763c004..4c7b32284 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -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 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 30628a3cd..dd4e4d568 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -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