Merge c1749cdf2e9b1459f4982545407c7e2d5238eda8 into 9969dc60c5278340b6b9a4d7facdde620e99d1f5

This commit is contained in:
吞云(TuYw) 2025-08-02 23:00:10 +08:00 committed by GitHub
commit 939f2330dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 128 additions and 17 deletions

View File

@ -87,6 +87,9 @@ public final class Config implements Observable {
@SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("bgImageOpacity")
private IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100);
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@ -274,6 +277,18 @@ public final class Config implements Observable {
this.backgroundImageUrl.set(backgroundImageUrl);
}
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

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

View File

@ -31,6 +31,9 @@ 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;
@ -55,6 +58,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;
@ -73,8 +77,8 @@ 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";
@ -124,18 +128,12 @@ 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().backgroundImageOpacityProperty().addListener(weakListener);
// pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> {
@ -196,6 +194,16 @@ public class DecoratorController {
@SuppressWarnings("FieldCanBeLocal") // Strong reference
private final InvalidationListener changeBackgroundListener;
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();
@ -224,12 +232,45 @@ 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));
}
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,10 +19,12 @@ 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.collections.FXCollections;
import javafx.geometry.Insets;
@ -30,10 +32,7 @@ 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 +55,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));
@ -127,7 +140,46 @@ 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().getBackgroundImageOpacity());
slider.setShowTickMarks(true);
slider.setMajorTickUnit(10);
slider.setMinorTickCount(1);
slider.setBlockIncrement(5);
slider.setSnapToTicks(true);
HBox.setHgrow(slider, Priority.ALWAYS);
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())));
config().backgroundImageTypeProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != EnumBackgroundImage.TRANSLUCENT && oldValue != EnumBackgroundImage.TRANSLUCENT)
return;
final int opacity = snapOpacity(slider.getValue());
if (newValue == EnumBackgroundImage.TRANSLUCENT) {
if (opacity == 100)
slider.setValue(50);
} else {
slider.setValue(100);
}
});
opacityItem.getChildren().setAll(label, slider, textOpacity);
}
componentList.getContent().setAll(backgroundItem, opacityItem);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
}

View File

@ -1353,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

@ -1150,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

@ -1160,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