添加“窗口透明”选项 (#6383)
This commit is contained in:
1 parent
dec6837528
commit
62d7b36248
17 files changed
+370
-81
No files matched your search
@@ -87,6 +87,9 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
/// Theme appearance override key for title-bar transparency.
|
||||
public static final String THEME_APPEARANCE_TITLE_BAR_TRANSPARENT = "titleBarTransparent";
|
||||
|
||||
/// Theme appearance override key for window transparency.
|
||||
public static final String THEME_APPEARANCE_WINDOW_TRANSPARENT = "windowTransparent";
|
||||
|
||||
/// Theme appearance override key for the primary background source.
|
||||
public static final String THEME_APPEARANCE_BACKGROUND = "background";
|
||||
|
||||
@@ -336,6 +339,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
return titleBarTransparent;
|
||||
}
|
||||
|
||||
/// Whether translucent launcher backgrounds reveal content behind the window.
|
||||
@SerializedName("windowTransparent")
|
||||
private final BooleanProperty windowTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
/// Returns the window transparency property.
|
||||
public BooleanProperty windowTransparentProperty() {
|
||||
return windowTransparent;
|
||||
}
|
||||
|
||||
// Background source
|
||||
|
||||
/// The launcher background source type.
|
||||
|
||||
@@ -255,6 +255,10 @@ public final class LegacyConfigMigrator {
|
||||
renameMember(jsonObject, "bgpaint", "customBackgroundPaint");
|
||||
renameMember(jsonObject, "backgroundPaint", "customBackgroundPaint");
|
||||
migrateBackgroundOpacity(jsonObject, themeAppearanceOverrides);
|
||||
jsonObject.addProperty("windowTransparent", true);
|
||||
addThemeAppearanceOverride(
|
||||
themeAppearanceOverrides,
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT);
|
||||
writeThemeAppearanceOverrides(jsonObject, themeAppearanceOverrides);
|
||||
renameMember(jsonObject, "proxyUserName", "proxyUser");
|
||||
migrateLegacySelectedVersions(jsonObject);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import javafx.scene.layout.Background;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// A launcher background and the opacity applied by its rendering node.
|
||||
///
|
||||
/// @param background the JavaFX background rendered by the launcher
|
||||
/// @param opacity the node opacity in the range `[0, 1]`
|
||||
@NotNullByDefault
|
||||
public record LauncherBackground(Background background, double opacity) {
|
||||
/// Creates a launcher background value.
|
||||
public LauncherBackground {
|
||||
Objects.requireNonNull(background);
|
||||
if (!Double.isFinite(opacity) || opacity < 0.0 || opacity > 1.0) {
|
||||
throw new IllegalArgumentException("Launcher background opacity must be between 0 and 1: " + opacity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/// @param contrast the MonetFX contrast level, or `null` when inherited
|
||||
/// @param background the background settings, or `null` when inherited
|
||||
/// @param titleBar the title-bar settings, or `null` when inherited
|
||||
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
|
||||
@NotNullByDefault
|
||||
public record ThemeAppearance(
|
||||
@Nullable ThemeColorSource color,
|
||||
@@ -50,7 +51,8 @@ public record ThemeAppearance(
|
||||
@Nullable ColorStyle colorStyle,
|
||||
@Nullable Contrast contrast,
|
||||
@Nullable ThemeBackgroundSettings background,
|
||||
@Nullable ThemeTitleBar titleBar) {
|
||||
@Nullable ThemeTitleBar titleBar,
|
||||
@Nullable Boolean windowTransparent) {
|
||||
|
||||
/// JSON member name for the color seed.
|
||||
static final String FIELD_COLOR = "color";
|
||||
@@ -70,6 +72,9 @@ public record ThemeAppearance(
|
||||
/// JSON member name for title-bar settings.
|
||||
static final String FIELD_TITLE_BAR = "titleBar";
|
||||
|
||||
/// JSON member name for window transparency.
|
||||
static final String FIELD_WINDOW_TRANSPARENT = "windowTransparent";
|
||||
|
||||
/// Creates an appearance patch.
|
||||
///
|
||||
/// @param color the color seed source, or `null` when inherited
|
||||
@@ -78,6 +83,7 @@ public record ThemeAppearance(
|
||||
/// @param contrast the MonetFX contrast level, or `null` when inherited
|
||||
/// @param background the background settings, or `null` when inherited
|
||||
/// @param titleBar the title-bar settings, or `null` when inherited
|
||||
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
|
||||
public ThemeAppearance {
|
||||
if (background != null && background.isEmpty()) {
|
||||
background = null;
|
||||
@@ -100,7 +106,8 @@ public record ThemeAppearance(
|
||||
readColorStyle(object),
|
||||
readContrast(object),
|
||||
readBackground(object),
|
||||
readTitleBar(object));
|
||||
readTitleBar(object),
|
||||
readWindowTransparent(object));
|
||||
}
|
||||
|
||||
/// Returns whether this appearance contains no concrete fields.
|
||||
@@ -112,7 +119,8 @@ public record ThemeAppearance(
|
||||
&& colorStyle == null
|
||||
&& contrast == null
|
||||
&& background == null
|
||||
&& titleBar == null;
|
||||
&& titleBar == null
|
||||
&& windowTransparent == null;
|
||||
}
|
||||
|
||||
/// Adds this appearance patch's concrete fields to a JSON object.
|
||||
@@ -139,6 +147,9 @@ public record ThemeAppearance(
|
||||
if (titleBar != null) {
|
||||
object.add(FIELD_TITLE_BAR, titleBar.toJsonObject());
|
||||
}
|
||||
if (windowTransparent != null) {
|
||||
object.addProperty(FIELD_WINDOW_TRANSPARENT, windowTransparent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this appearance patch to its JSON representation.
|
||||
@@ -167,7 +178,8 @@ public record ThemeAppearance(
|
||||
: patch.background != null ? patch.background : background,
|
||||
titleBar != null && patch.titleBar != null
|
||||
? titleBar.merge(patch.titleBar)
|
||||
: patch.titleBar != null ? patch.titleBar : titleBar);
|
||||
: patch.titleBar != null ? patch.titleBar : titleBar,
|
||||
patch.windowTransparent != null ? patch.windowTransparent : windowTransparent);
|
||||
}
|
||||
|
||||
/// Converts this appearance to concrete launcher theme values.
|
||||
@@ -309,6 +321,19 @@ public record ThemeAppearance(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the optional window transparency field.
|
||||
private static @Nullable Boolean readWindowTransparent(JsonObject object) {
|
||||
JsonElement element = object.get(FIELD_WINDOW_TRANSPARENT);
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(element instanceof JsonPrimitive primitive) || !primitive.isBoolean()) {
|
||||
LOG.warning("Ignored invalid theme windowTransparent: expected a boolean, got " + element);
|
||||
return null;
|
||||
}
|
||||
return primitive.getAsBoolean();
|
||||
}
|
||||
|
||||
/// Reads an optional string field.
|
||||
private static @Nullable String readString(JsonObject object, String field) {
|
||||
JsonElement element = object.get(field);
|
||||
|
||||
@@ -660,7 +660,8 @@ public final class ThemePackManager {
|
||||
currentColorStyle(),
|
||||
null,
|
||||
background,
|
||||
new ThemeTitleBar(currentTitleBarTransparent()));
|
||||
new ThemeTitleBar(currentTitleBarTransparent()),
|
||||
currentWindowTransparent());
|
||||
Theme theme = new Theme(
|
||||
null,
|
||||
null,
|
||||
@@ -755,6 +756,13 @@ public final class ThemePackManager {
|
||||
: Objects.requireNonNullElse(resolveCurrentTitleBarTransparent(currentResolveContext()), false);
|
||||
}
|
||||
|
||||
/// Returns whether the current launcher window should be transparent.
|
||||
private static boolean currentWindowTransparent() throws IOException {
|
||||
return SettingsManager.settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)
|
||||
? settings().windowTransparentProperty().get()
|
||||
: Objects.requireNonNullElse(resolveCurrentWindowTransparent(currentResolveContext()), false);
|
||||
}
|
||||
|
||||
/// Resolves the selected theme's controlled brightness.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
@@ -795,6 +803,16 @@ public final class ThemePackManager {
|
||||
return appearance != null && appearance.titleBar() != null ? appearance.titleBar().transparent() : null;
|
||||
}
|
||||
|
||||
/// Resolves the selected theme window transparency directive.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
/// @return the selected theme window transparency directive, or `null` when unavailable
|
||||
/// @throws IOException if the selected theme pack cannot be read
|
||||
public static @Nullable Boolean resolveCurrentWindowTransparent(ThemeResolveContext context) throws IOException {
|
||||
@Nullable ThemeAppearance appearance = resolveCurrentThemeAppearance(context);
|
||||
return appearance != null ? appearance.windowTransparent() : null;
|
||||
}
|
||||
|
||||
/// Resolves the current selected theme color source without extracting any wallpaper pixels.
|
||||
///
|
||||
/// @param context the condition resolution context
|
||||
|
||||
@@ -32,9 +32,6 @@ import javafx.collections.SetChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.BackgroundImage;
|
||||
@@ -282,9 +279,11 @@ public final class Themes {
|
||||
colorScheme
|
||||
);
|
||||
|
||||
/// The current JavaFX launcher background.
|
||||
private static final ReadOnlyObjectWrapper<Background> background = new ReadOnlyObjectWrapper<>(
|
||||
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
/// The current JavaFX launcher background and its node opacity.
|
||||
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
|
||||
new LauncherBackground(
|
||||
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)),
|
||||
1.0));
|
||||
|
||||
/// The last loaded background source used to rebuild the JavaFX background without reloading images.
|
||||
private static @Nullable LoadedBackground loadedBackground;
|
||||
@@ -300,8 +299,8 @@ public final class Themes {
|
||||
|
||||
/// A loaded JavaFX background source and the wallpaper color extracted while loading it.
|
||||
private sealed interface LoadedBackground {
|
||||
/// Returns the loaded JavaFX background.
|
||||
Background background();
|
||||
/// Returns the loaded JavaFX background and its node opacity.
|
||||
LauncherBackground background();
|
||||
|
||||
/// Returns whether this background is the configured fallback background.
|
||||
boolean fallbackBackground();
|
||||
@@ -316,7 +315,7 @@ public final class Themes {
|
||||
/// @param wallpaperThemeColor the seed color extracted from [#image], or `null` when unavailable
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record Image(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
javafx.scene.image.Image image,
|
||||
@Nullable ThemeColor wallpaperThemeColor,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
@@ -339,7 +338,7 @@ public final class Themes {
|
||||
/// @param wallpaperThemeColor the seed color represented by [#paint], or `null` when unavailable
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record Paint(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
@Nullable javafx.scene.paint.Paint paint,
|
||||
@Nullable ThemeColor wallpaperThemeColor,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
@@ -359,7 +358,7 @@ public final class Themes {
|
||||
/// @param background the loaded JavaFX background
|
||||
/// @param fallbackBackground whether this background is the configured fallback background
|
||||
record ThemeColorFill(
|
||||
Background background,
|
||||
LauncherBackground background,
|
||||
boolean fallbackBackground) implements LoadedBackground {
|
||||
/// Creates a loaded theme-color background.
|
||||
///
|
||||
@@ -543,8 +542,8 @@ public final class Themes {
|
||||
return colorScheme.get();
|
||||
}
|
||||
|
||||
/// Returns the current JavaFX launcher background property.
|
||||
public static ReadOnlyObjectProperty<Background> backgroundProperty() {
|
||||
/// Returns the current JavaFX launcher background and node opacity property.
|
||||
public static ReadOnlyObjectProperty<LauncherBackground> backgroundProperty() {
|
||||
startBackgroundUpdates();
|
||||
return background.getReadOnlyProperty();
|
||||
}
|
||||
@@ -824,7 +823,8 @@ public final class Themes {
|
||||
}
|
||||
@Nullable LoadedBackground loaded = loadedBackground;
|
||||
if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
|
||||
Background newBackground = createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
|
||||
LauncherBackground newBackground =
|
||||
createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
|
||||
loadedBackground = new LoadedBackground.ThemeColorFill(newBackground, themeColorFill.fallbackBackground());
|
||||
background.set(newBackground);
|
||||
}
|
||||
@@ -858,17 +858,23 @@ public final class Themes {
|
||||
LoadedBackground refreshed;
|
||||
if (loaded instanceof LoadedBackground.Image imageBackground) {
|
||||
refreshed = new LoadedBackground.Image(
|
||||
createBackgroundWithOpacity(imageBackground.image(), opacity),
|
||||
new LauncherBackground(
|
||||
imageBackground.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
imageBackground.image(),
|
||||
imageBackground.wallpaperThemeColor(),
|
||||
imageBackground.fallbackBackground());
|
||||
} else if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
|
||||
refreshed = new LoadedBackground.ThemeColorFill(
|
||||
createThemeColorBackground(opacity),
|
||||
new LauncherBackground(
|
||||
themeColorFill.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
themeColorFill.fallbackBackground());
|
||||
} else if (loaded instanceof LoadedBackground.Paint paintBackground) {
|
||||
refreshed = new LoadedBackground.Paint(
|
||||
createPaintBackground(paintBackground.paint(), opacity),
|
||||
new LauncherBackground(
|
||||
paintBackground.background().background(),
|
||||
MathUtils.clamp(opacity, 0., 1.)),
|
||||
paintBackground.paint(),
|
||||
paintBackground.wallpaperThemeColor(),
|
||||
paintBackground.fallbackBackground());
|
||||
@@ -917,60 +923,37 @@ public final class Themes {
|
||||
return FXUtils.loadImage(new CacheFileTask(url).run());
|
||||
}
|
||||
|
||||
/// Creates a JavaFX paint background with the requested opacity.
|
||||
private static Background createPaintBackground(@Nullable Paint paint, double opacity) {
|
||||
opacity = MathUtils.clamp(opacity, 0., 1.);
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null) {
|
||||
color = Color.WHITE;
|
||||
}
|
||||
if (opacity < 1.) {
|
||||
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
|
||||
}
|
||||
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else {
|
||||
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
/// Creates a JavaFX paint background with a separately stored node opacity.
|
||||
private static LauncherBackground createPaintBackground(@Nullable Paint paint, double opacity) {
|
||||
return new LauncherBackground(
|
||||
new Background(new BackgroundFill(
|
||||
Objects.requireNonNullElse(paint, Color.WHITE),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
MathUtils.clamp(opacity, 0., 1.));
|
||||
}
|
||||
|
||||
/// Creates a JavaFX background from the current theme color scheme surface container.
|
||||
private static Background createThemeColorBackground(double opacity) {
|
||||
private static LauncherBackground createThemeColorBackground(double opacity) {
|
||||
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
|
||||
}
|
||||
|
||||
/// Creates a JavaFX image background with the requested opacity.
|
||||
private static Background createBackgroundWithOpacity(Image image, double opacity) {
|
||||
PixelReader pixelReader = image.getPixelReader();
|
||||
if (opacity <= 0) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else if (opacity >= 1. || pixelReader == null) {
|
||||
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());
|
||||
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 Background(new BackgroundImage(
|
||||
tempImage,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
}
|
||||
/// Creates a JavaFX image background with a separately stored node opacity.
|
||||
private static LauncherBackground createBackgroundWithOpacity(Image image, double opacity) {
|
||||
Background imageBackground = new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(
|
||||
BackgroundSize.AUTO,
|
||||
BackgroundSize.AUTO,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true)
|
||||
));
|
||||
return new LauncherBackground(imageBackground, MathUtils.clamp(opacity, 0., 1.));
|
||||
}
|
||||
|
||||
/// Loads a default launcher background image from local workspace files.
|
||||
@@ -1073,6 +1056,27 @@ public final class Themes {
|
||||
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
|
||||
);
|
||||
|
||||
/// Whether the launcher window should be transparent after applying launcher and theme settings.
|
||||
private static final BooleanBinding windowTransparent = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)) {
|
||||
return settings().windowTransparentProperty().get();
|
||||
}
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentWindowTransparent(ThemePackManager.currentResolveContext()),
|
||||
false);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
settings().windowTransparentProperty(),
|
||||
settings().getThemeAppearanceOverrides(),
|
||||
settings().selectedThemeProperty(),
|
||||
settings().themeBrightnessModeProperty(),
|
||||
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
|
||||
);
|
||||
|
||||
/// The title text fill derived from the current color scheme.
|
||||
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
|
||||
() -> titleBarTransparent.get()
|
||||
@@ -1092,6 +1096,11 @@ public final class Themes {
|
||||
return titleBarTransparent;
|
||||
}
|
||||
|
||||
/// Returns whether the launcher window should be transparent after applying launcher and theme settings.
|
||||
public static BooleanBinding windowTransparentProperty() {
|
||||
return windowTransparent;
|
||||
}
|
||||
|
||||
/// Returns whether the current color scheme uses dark brightness.
|
||||
public static BooleanBinding darkModeProperty() {
|
||||
return darkMode;
|
||||
|
||||
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.theme.LauncherBackground;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
@@ -49,7 +50,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
public class Decorator extends Control {
|
||||
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
|
||||
|
||||
/// The launcher background and opacity rendered by this decorator's skin.
|
||||
private final ObjectProperty<LauncherBackground> contentBackground = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
|
||||
@@ -151,15 +154,18 @@ public class Decorator extends Control {
|
||||
this.container.set(container);
|
||||
}
|
||||
|
||||
public Background getContentBackground() {
|
||||
/// Returns the launcher background rendered behind the content.
|
||||
public LauncherBackground getContentBackground() {
|
||||
return contentBackground.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Background> contentBackgroundProperty() {
|
||||
/// Returns the launcher background property rendered by this decorator's skin.
|
||||
public ObjectProperty<LauncherBackground> contentBackgroundProperty() {
|
||||
return contentBackground;
|
||||
}
|
||||
|
||||
public void setContentBackground(Background contentBackground) {
|
||||
/// Sets the launcher background rendered behind the content.
|
||||
public void setContentBackground(LauncherBackground contentBackground) {
|
||||
this.contentBackground.set(contentBackground);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -134,9 +135,33 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
|
||||
StackPane wrapper = new StackPane();
|
||||
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? null
|
||||
: skinnable.getContentBackground().background(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? 1.0
|
||||
: skinnable.getContentBackground().opacity(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
|
||||
BorderPane frame = new BorderPane();
|
||||
frame.getStyleClass().addAll("jfx-decorator");
|
||||
wrapper.getChildren().setAll(frame);
|
||||
wrapper.getChildren().setAll(backgroundNode, frame);
|
||||
skinnable.setDrawerWrapper(wrapper);
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
@@ -178,20 +203,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
titleContainer = new StackPane();
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.max(0.0, wrapper.getHeight()
|
||||
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
|
||||
wrapper.heightProperty(),
|
||||
skinnable.titleTransparentProperty(),
|
||||
titleContainer.heightProperty()));
|
||||
|
||||
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
|
||||
// and decide whether the whole top bar should be rendered in white or black. TODO
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
container.backgroundProperty().unbind();
|
||||
container.setBackground(null);
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
} else {
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
wrapper.backgroundProperty().unbind();
|
||||
wrapper.setBackground(null);
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -214,6 +215,60 @@ public class PersonalizationPage extends StackPane {
|
||||
refresh.invalidated(null);
|
||||
}
|
||||
|
||||
/// Binds a line toggle button to an inheritable theme appearance setting.
|
||||
private static void bindThemeAppearanceToggleButton(
|
||||
LineToggleButton button,
|
||||
String setting,
|
||||
BooleanProperty directProperty,
|
||||
Supplier<Boolean> effectiveValueSupplier) {
|
||||
JFXButton inheritButton = createThemeAppearanceOverrideButton();
|
||||
button.setTitleTrailing(inheritButton);
|
||||
|
||||
Holder<Boolean> updating = new Holder<>(false);
|
||||
InvalidationListener refresh = ignored -> {
|
||||
if (updating.value) {
|
||||
return;
|
||||
}
|
||||
updating.value = true;
|
||||
try {
|
||||
boolean overridden = settings().getThemeAppearanceOverrides().contains(setting);
|
||||
button.setSelected(overridden ? directProperty.get() : effectiveValueSupplier.get());
|
||||
updateThemeAppearanceOverrideButton(inheritButton, !overridden);
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
button.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (updating.value) {
|
||||
return;
|
||||
}
|
||||
updating.value = true;
|
||||
try {
|
||||
directProperty.set(Boolean.TRUE.equals(newValue));
|
||||
settings().getThemeAppearanceOverrides().add(setting);
|
||||
updateThemeAppearanceOverrideButton(inheritButton, false);
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
refresh.invalidated(null);
|
||||
});
|
||||
directProperty.addListener(refresh);
|
||||
addThemeAppearanceRefreshListener(refresh);
|
||||
|
||||
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (!settings().getThemeAppearanceOverrides().contains(setting)) {
|
||||
directProperty.set(button.isSelected());
|
||||
settings().getThemeAppearanceOverrides().add(setting);
|
||||
} else {
|
||||
settings().getThemeAppearanceOverrides().remove(setting);
|
||||
}
|
||||
refresh.invalidated(null);
|
||||
event.consume();
|
||||
});
|
||||
refresh.invalidated(null);
|
||||
}
|
||||
|
||||
/// Returns the display name for the currently selected launcher theme.
|
||||
private static String getSelectedThemeTitle() {
|
||||
ThemeReference reference = settings().getSelectedThemeOrDefault();
|
||||
@@ -1078,6 +1133,25 @@ public class PersonalizationPage extends StackPane {
|
||||
refresh.invalidated(null);
|
||||
themeAppearanceList.getContent().add(titleBarTransparentButton);
|
||||
}
|
||||
|
||||
LineToggleButton windowTransparentButton = new LineToggleButton();
|
||||
windowTransparentButton.setTitle(i18n("settings.launcher.window_transparent"));
|
||||
bindThemeAppearanceToggleButton(
|
||||
windowTransparentButton,
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT,
|
||||
settings().windowTransparentProperty(),
|
||||
() -> {
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentWindowTransparent(
|
||||
ThemePackManager.currentResolveContext()),
|
||||
false);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
themeAppearanceList.getContent().add(windowTransparentButton);
|
||||
|
||||
content.getChildren().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")),
|
||||
themeAppearanceList,
|
||||
|
||||
@@ -1609,6 +1609,7 @@ settings.launcher.theme_color_type.default=Default
|
||||
settings.launcher.title_transparent=Transparent Titlebar
|
||||
settings.launcher.turn_off_animations=Disable Animation
|
||||
settings.launcher.version_list_source=Version List
|
||||
settings.launcher.window_transparent=Transparent Window
|
||||
settings.launcher.background.settings.opacity=Opacity
|
||||
|
||||
settings.memory=Memory
|
||||
|
||||
@@ -1400,6 +1400,7 @@ settings.launcher.theme_color_type.default=預設
|
||||
settings.launcher.title_transparent=標題欄透明
|
||||
settings.launcher.turn_off_animations=關閉動畫
|
||||
settings.launcher.version_list_source=版本清單來源
|
||||
settings.launcher.window_transparent=視窗透明
|
||||
settings.launcher.background.settings.opacity=不透明度
|
||||
|
||||
settings.memory=遊戲記憶體
|
||||
|
||||
@@ -1405,6 +1405,7 @@ settings.launcher.theme_color_type.default=默认
|
||||
settings.launcher.title_transparent=标题栏透明
|
||||
settings.launcher.turn_off_animations=关闭动画
|
||||
settings.launcher.version_list_source=版本列表源
|
||||
settings.launcher.window_transparent=窗口透明
|
||||
settings.launcher.background.settings.opacity=不透明度
|
||||
|
||||
settings.memory=游戏内存
|
||||
|
||||
@@ -137,6 +137,7 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertEquals(ThemeColor.of("#336699"), launcherSettings.customThemeColorProperty().get());
|
||||
assertEquals(ThemeColorType.CUSTOM, launcherSettings.themeColorTypeProperty().get());
|
||||
assertTrue(launcherSettings.titleBarTransparentProperty().get());
|
||||
assertTrue(launcherSettings.windowTransparentProperty().get());
|
||||
assertEquals("/pictures/background.png", launcherSettings.customBackgroundImagePathProperty().get());
|
||||
assertEquals("https://example.com/background.png",
|
||||
launcherSettings.networkBackgroundImageUrlProperty().get());
|
||||
@@ -148,6 +149,8 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_COLOR));
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
|
||||
LauncherSettings.THEME_APPEARANCE_TITLE_BAR_TRANSPARENT));
|
||||
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
|
||||
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT));
|
||||
assertFalse(launcherSettings.getThemeAppearanceOverrides().contains("networkBackgroundImageCachePolicy"));
|
||||
assertEquals("/pictures/background.png", serialized.get("customBackgroundImagePath").getAsString());
|
||||
assertEquals("https://example.com/background.png",
|
||||
@@ -158,6 +161,7 @@ public final class LauncherSettingsMigrationTest {
|
||||
assertEquals(ThemeColorType.CUSTOM.name(), serialized.get("themeColorType").getAsString());
|
||||
assertEquals("dark", serialized.get("themeBrightnessMode").getAsString());
|
||||
assertTrue(serialized.get("titleBarTransparent").getAsBoolean());
|
||||
assertTrue(serialized.get("windowTransparent").getAsBoolean());
|
||||
assertEquals("#336699", serialized.get("customBackgroundPaint").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/// Tests launcher background opacity storage.
|
||||
@NotNullByDefault
|
||||
public final class LauncherBackgroundTest {
|
||||
/// Verifies opacity changes reuse the unmodified JavaFX background.
|
||||
@Test
|
||||
public void changesOpacityWithoutChangingBackground() {
|
||||
Background background = new Background(
|
||||
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
LauncherBackground changed = new LauncherBackground(background, 0.4);
|
||||
|
||||
assertSame(background, changed.background());
|
||||
assertEquals(0.4, changed.opacity());
|
||||
assertEquals(1.0, ((Color) changed.background().getFills().get(0).getFill()).getOpacity());
|
||||
}
|
||||
|
||||
/// Verifies invalid node opacity values are rejected.
|
||||
@Test
|
||||
public void rejectsInvalidOpacity() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, -0.1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, 1.1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, Double.NaN));
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public final class ThemePackManifestTest {
|
||||
|
||||
assertEquals(BackgroundType.DEFAULT, background.type());
|
||||
assertEquals(1.0, background.opacity(), 0.0);
|
||||
assertNull(theme.appearance().windowTransparent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
"themeColor",
|
||||
"themeColorStyle",
|
||||
"titleBarTransparent",
|
||||
"windowTransparent",
|
||||
"background",
|
||||
"backgroundOpacity"
|
||||
]
|
||||
@@ -137,6 +138,9 @@
|
||||
"launcherFontFamily": {
|
||||
"type": "string"
|
||||
},
|
||||
"windowTransparent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"animationDisabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -174,6 +174,9 @@
|
||||
"titleBar": {
|
||||
"$ref": "#/$defs/titleBar"
|
||||
},
|
||||
"windowTransparent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -217,6 +220,11 @@
|
||||
"required": [
|
||||
"titleBar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"windowTransparent"
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -240,6 +248,9 @@
|
||||
},
|
||||
"titleBar": {
|
||||
"$ref": "#/$defs/titleBar"
|
||||
},
|
||||
"windowTransparent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user