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 f9611ed483..3d42485cdc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -85,6 +85,9 @@ public static Config fromJson(String json) throws JsonParseException { @SerializedName("bgurl") private StringProperty backgroundImageUrl = new SimpleStringProperty(); + @SerializedName("bgImageOpacity") + private DoubleProperty backgroundImageOpacity = new SimpleDoubleProperty(1.00); + @SerializedName("commonDirType") private ObjectProperty commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT); @@ -277,6 +280,18 @@ public void setBackgroundImageUrl(String backgroundImageUrl) { this.backgroundImageUrl.set(backgroundImageUrl); } + public Double getBackgroundImageOpacity(){ + return backgroundImageOpacity.get(); + } + + public void setBackgroundImageOpacity(double backgroundImageOpacity){ + this.backgroundImageOpacity.set(backgroundImageOpacity); + } + + public DoubleProperty 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 9b07239994..1da09895dc 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 @@ -28,6 +28,9 @@ 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.layout.*; @@ -68,8 +71,8 @@ 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"; @@ -98,14 +101,17 @@ public DecoratorController(Stage stage, Node mainPage) { // 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(); + changeSettingsListener = (observable, oldValue, newValue) -> { + double newValueDouble = newValue.doubleValue(); + if (Double.isNaN(lastValue) || Math.abs(newValueDouble - lastValue) > threshold) { + updateBackground(); + lastValue = newValueDouble; + } else if (config().getBackgroundImageOpacity() == 0 || config().getBackgroundImageOpacity() == 1) { + updateBackground(); + } }; + config().backgroundImageOpacityProperty().addListener(changeSettingsListener); WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener); config().backgroundImageTypeProperty().addListener(weakListener); config().backgroundImageProperty().addListener(weakListener); @@ -155,8 +161,24 @@ public Decorator getDecorator() { //FXThread private int changeBackgroundCount = 0; + private double lastValue = Double.NaN; + private final double threshold = 0.05;//Determine how often the background is refreshed when the opacity is modified + + @SuppressWarnings("FieldCanBeLocal") // Strong reference private final InvalidationListener changeBackgroundListener; + @SuppressWarnings("FieldCanBeLocal") + private final ChangeListener changeSettingsListener; + + private void updateBackground() { + LOG.info("updateBackground"); + final int currentCount = ++this.changeBackgroundCount; + CompletableFuture.supplyAsync(this::getBackground, Schedulers.io()) + .thenAcceptAsync(background -> { + if (this.changeBackgroundCount == currentCount) + decorator.setContentBackground(background); + }, Schedulers.javafx()); + } private Background getBackground() { EnumBackgroundImage imageType = config().getBackgroundImageType(); @@ -197,7 +219,47 @@ private Background getBackground() { 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, double opacity) { + if (opacity == 0){ + return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY)); + } + if (opacity == 1) { + return new Background(new BackgroundImage( + image, + BackgroundRepeat.NO_REPEAT, + BackgroundRepeat.NO_REPEAT, + BackgroundPosition.DEFAULT, + new BackgroundSize(800, 480, false, false, true, true) + )); + } + LOG.info(String.valueOf(opacity)); + BackgroundImage backgroundImage = getImageWithOpacity(image, opacity); + + return new Background(backgroundImage); + } + + private static BackgroundImage getImageWithOpacity(Image image, double opacity) { + 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); + pixelWriter.setColor(x, y, newColor); + } + } + + return 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 f1938c8efc..9ea736685c 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 @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXSlider; import com.jfoenix.controls.JFXTextField; import com.jfoenix.effects.JFXDepthManager; import javafx.application.Platform; @@ -28,10 +29,7 @@ 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 org.jackhuang.hmcl.setting.EnumBackgroundImage; @@ -43,7 +41,10 @@ import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -126,6 +127,66 @@ public PersonalizationPage() { content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList); } + { + VBox bgSettings = new VBox(16); + bgSettings.getStyleClass().add("card-non-transparent"); + { + HBox hbox = new HBox(8); + hbox.setAlignment(Pos.CENTER); + hbox.setPadding(new Insets(0, 0, 0, 10)); + + Label label1 = new Label(i18n("settings.launcher.background.settings.opacity")); + Label label2 = new Label("%"); + + double opa = config().getBackgroundImageOpacity(); + JFXSlider slider = new JFXSlider(0, 1, opa); + HBox.setHgrow(slider, Priority.ALWAYS); + + JFXTextField textOpacity = new JFXTextField(); + textOpacity.setText(BigDecimal.valueOf(opa * 100).setScale(2, RoundingMode.HALF_UP).toString()); + textOpacity.setPrefWidth(60); + + AtomicReference lastValidOpacity = new AtomicReference<>(slider.getValue()); + slider.valueProperty().addListener((observable, oldValue, newValue) -> { + double opacity = newValue.doubleValue(); + textOpacity.setText(BigDecimal.valueOf(opacity * 100).setScale(2, RoundingMode.HALF_UP).toString()); + lastValidOpacity.set(opacity); + config().setBackgroundImageOpacity(opacity); + }); + textOpacity.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue){ + try { + String text = textOpacity.getText().trim(); + double opacity = Double.parseDouble(text) / 100; + if (opacity >= 0 && opacity <= 1) { + slider.setValue(opacity); + } else if (opacity < 0) { + slider.setValue(0); + textOpacity.setText("0.00"); + } else if (opacity > 1) { + slider.setValue(1); + textOpacity.setText("100.00"); + } + } catch (NumberFormatException ignored) { + slider.setValue(lastValidOpacity.get()); + textOpacity.setText(BigDecimal.valueOf(lastValidOpacity.get() * 100).setScale(2, RoundingMode.HALF_UP).toString()); + } + } + }); + + slider.setValueFactory(slider1 -> Bindings.createStringBinding(() -> String.format("%.2f", slider1.getValue()), slider1.valueProperty())); + + HBox.setMargin(label2, new Insets(0, 10, 0, 0)); + + hbox.getChildren().setAll(label1, slider, textOpacity, label2); + bgSettings.getChildren().add(hbox); + } + //hide the opacity setting when selecting a translucency type + config().backgroundImageTypeProperty().addListener((observable, oldValue, newValue) -> bgSettings.setVisible(newValue != EnumBackgroundImage.TRANSLUCENT)); + content.getChildren().add(bgSettings); + } + + { ComponentList logPane = new ComponentSublist(); logPane.setTitle(i18n("settings.launcher.log")); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index dc1bdd651e..3ab4c5d86c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1164,6 +1164,7 @@ settings.launcher.theme=Theme settings.launcher.title_transparent=Transparent titlebar settings.launcher.turn_off_animations=Turn off 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 GB Minimum / %2$.1f GB Allocated diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index a11eb2474b..1fa62502e0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1022,6 +1022,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 GB / 實際分配 %2$.1f GB 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 ededc00cd6..1b9e6a3f66 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1021,6 +1021,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 GB / 实际分配 %2$.1f GB