@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -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=キャッシュするディレクトリを選択します
|
||||
|
||||
@@ -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=Выберите папку для кэша
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user