From c360a4abb096307d70641790127ba94e0592ebc1 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Mon, 6 Apr 2020 17:33:57 +0800 Subject: [PATCH] alt: transparent title bar --- .../org/jackhuang/hmcl/ui/Controllers.java | 2 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 28 ++++++- .../hmcl/ui/animation/TransitionPane.java | 14 +--- .../hmcl/ui/decorator/DecoratorPage.java | 16 ++++ .../hmcl/ui/decorator/DecoratorSkin.java | 75 ++++++++++++++----- .../ui/decorator/DecoratorTransitionPage.java | 37 +++++++-- .../org/jackhuang/hmcl/ui/main/MainPage.java | 2 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 17 ++--- HMCL/src/main/resources/assets/css/root.css | 18 +++-- 9 files changed, 146 insertions(+), 63 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 89da51ab4..fcd035d6f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -110,7 +110,7 @@ public final class Controllers { Task.runAsync(JavaVersion::initialize).start(); - scene = new Scene(decorator.getDecorator(), 800, 519); + scene = new Scene(decorator.getDecorator(), 800, 480); decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty()); decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty()); scene.getStylesheets().setAll(config().getTheme().getStylesheets()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index b5f6f1d19..e879ead17 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -18,16 +18,14 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.*; -import javafx.animation.Animation; -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; +import javafx.animation.*; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.property.Property; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; +import javafx.beans.value.WritableValue; import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.geometry.Insets; @@ -62,6 +60,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URI; import java.util.List; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; @@ -304,6 +303,27 @@ public final class FXUtils { }); } + public static void playAnimation(Node node, String animationKey, Timeline timeline) { + animationKey = "FXUTILS.ANIMATION." + animationKey; + Object oldTimeline = node.getProperties().get(animationKey); + if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop(); + if (timeline != null) timeline.play(); + node.getProperties().put(animationKey, timeline); + } + + public static void playAnimation(Node node, String animationKey, Duration duration, WritableValue property, T from, T to, Interpolator interpolator) { + if (from == null) from = property.getValue(); + if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) { + playAnimation(node, animationKey, null); + property.setValue(to); + } else { + playAnimation(node, animationKey, new Timeline( + new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)), + new KeyFrame(duration, new KeyValue(property, to, interpolator)) + )); + } + } + public static void openFolder(File file) { if (!FileUtils.makeDirectory(file)) { Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java index 9a5f15339..f5376149e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java @@ -26,7 +26,6 @@ import javafx.util.Duration; import org.jackhuang.hmcl.ui.FXUtils; public class TransitionPane extends StackPane implements AnimationHandler { - private Timeline animation; private Duration duration; private Node previousNode, currentNode; @@ -62,24 +61,19 @@ public class TransitionPane extends StackPane implements AnimationHandler { public void setContent(Node newView, AnimationProducer transition, Duration duration) { this.duration = duration; - Timeline prev = animation; - if (prev != null) - prev.stop(); - updateContent(newView); transition.init(this); // runLater or "init" will not work Platform.runLater(() -> { - Timeline nowAnimation = new Timeline(); - nowAnimation.getKeyFrames().addAll(transition.animate(this)); - nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> { + Timeline newAnimation = new Timeline(); + newAnimation.getKeyFrames().addAll(transition.animate(this)); + newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> { setMouseTransparent(false); getChildren().remove(previousNode); })); - nowAnimation.play(); - animation = nowAnimation; + FXUtils.playAnimation(this, "transition_pane", newAnimation); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java index 255c3e700..17d60eed2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java @@ -50,13 +50,21 @@ public interface DecoratorPage extends Refreshable { private final boolean backable; private final boolean refreshable; private final boolean animate; + private final boolean titleBarTransparent; + private final double leftPaneWidth; public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) { + this(title, titleNode, backable, refreshable, animate, false, 0); + } + + public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, boolean titleBarTransparent, double leftPaneWidth) { this.title = title; this.titleNode = titleNode; this.backable = backable; this.refreshable = refreshable; this.animate = animate; + this.titleBarTransparent = titleBarTransparent; + this.leftPaneWidth = leftPaneWidth; } public static State fromTitle(String title) { @@ -86,5 +94,13 @@ public interface DecoratorPage extends Refreshable { public boolean isAnimate() { return animate; } + + public boolean isTitleBarTransparent() { + return titleBarTransparent; + } + + public double getLeftPaneWidth() { + return leftPaneWidth; + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java index 2bf1c16c3..6261e70c1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.svg.SVGGlyph; import javafx.beans.binding.Bindings; import javafx.collections.ListChangeListener; +import javafx.css.PseudoClass; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -31,11 +32,11 @@ import javafx.scene.control.SkinBase; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; +import javafx.util.Duration; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -44,16 +45,19 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.util.Lang; public class DecoratorSkin extends SkinBase { + private static final PseudoClass TRANSPARENT = PseudoClass.getPseudoClass("transparent"); private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE), glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); }); private final BorderPane root; - private final BorderPane titleContainer; + private final StackPane titleContainer; private final Stage primaryStage; private final TransitionPane navBarPane; + private final StackPane leftPane; private double xOffset, yOffset, newX, newY, initX, initY; private boolean allowMove, isDragging; + private boolean titleBarTransparent = true; /** * Constructor for all SkinBase instances. @@ -68,23 +72,32 @@ public class DecoratorSkin extends SkinBase { minus.fillProperty().bind(Theme.foregroundFillBinding()); Decorator skinnable = getSkinnable(); + StackPane parent = new StackPane(); + parent.backgroundProperty().bind(skinnable.backgroundProperty()); + parent.setPickOnBounds(false); + parent.prefHeightProperty().bind(control.prefHeightProperty()); + parent.prefWidthProperty().bind(control.prefWidthProperty()); root = new BorderPane(); root.getStyleClass().addAll("jfx-decorator", "resize-border"); - root.prefHeightProperty().bind(control.prefHeightProperty()); - root.prefWidthProperty().bind(control.prefWidthProperty()); - root.setMaxHeight(Region.USE_PREF_SIZE); - root.setMinHeight(Region.USE_PREF_SIZE); - root.setMaxWidth(Region.USE_PREF_SIZE); - root.setMinWidth(Region.USE_PREF_SIZE); - // center node with a container node in bottom layer and a "welcome" layer at the top layer. + // animation layer at bottom + { + HBox layer = new HBox(); + leftPane = new StackPane(); + leftPane.setPrefWidth(0); + leftPane.getStyleClass().add("jfx-decorator-drawer"); + layer.getChildren().setAll(leftPane); + parent.getChildren().add(layer); + parent.getChildren().add(root); + } + + // center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top. StackPane container = new StackPane(); skinnable.setDrawerWrapper(container); - container.getStyleClass().add("jfx-decorator-drawer"); - container.backgroundProperty().bind(skinnable.backgroundProperty()); FXUtils.setOverflowHidden(container); - // bottom layer + + // content layer at middle { StackPane contentPlaceHolder = new StackPane(); contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container"); @@ -93,7 +106,7 @@ public class DecoratorSkin extends SkinBase { container.getChildren().add(contentPlaceHolder); } - // top layer for welcome and hint + // welcome and hint layer at top { StackPane floatLayer = new StackPane(); Bindings.bindContent(floatLayer.getChildren(), skinnable.containerProperty()); @@ -114,18 +127,26 @@ public class DecoratorSkin extends SkinBase { root.setCenter(container); - titleContainer = new BorderPane(); + titleContainer = new StackPane(); root.setOnMouseReleased(this::onMouseReleased); root.setOnMouseDragged(this::onMouseDragged); root.setOnMouseMoved(this::onMouseMoved); titleContainer.setPickOnBounds(false); titleContainer.setMinHeight(40); - titleContainer.getStyleClass().addAll("jfx-tool-bar", "window-title-bar"); + titleContainer.getStyleClass().addAll("jfx-tool-bar"); titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true); titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { if (!isDragging) allowMove = false; }); + StackPane titleBarBackground = new StackPane(); + titleBarBackground.getStyleClass().add("background"); + titleContainer.getChildren().add(titleBarBackground); + + BorderPane titleBar = new BorderPane(); + titleBar.setPickOnBounds(false); + titleContainer.getChildren().add(titleBar); + Rectangle rectangle = new Rectangle(0, 0, 0, 0); rectangle.widthProperty().bind(titleContainer.widthProperty()); rectangle.heightProperty().bind(titleContainer.heightProperty().add(100)); @@ -134,14 +155,21 @@ public class DecoratorSkin extends SkinBase { navBarPane = new TransitionPane(); FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> { if (s == null) return; - Node node = createNavBar(skinnable, s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode()); + Node node = createNavBar(skinnable, s.isTitleBarTransparent(), s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode()); + double targetOpacity = s.isTitleBarTransparent() ? 0 : 1; if (s.isAnimate()) { navBarPane.setContent(node, ContainerAnimations.FADE.getAnimationProducer()); } else { navBarPane.getChildren().setAll(node); } + + FXUtils.playAnimation(titleBarBackground, "animation", + s.isAnimate() ? Duration.millis(160) : null, titleBarBackground.opacityProperty(), null, targetOpacity, FXUtils.SINE); + titleBarTransparent = s.isTitleBarTransparent(); + FXUtils.playAnimation(leftPane, "animation", + s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE); }); - titleContainer.setCenter(navBarPane); + titleBar.setCenter(navBarPane); HBox buttonsContainer = new HBox(); buttonsContainer.setStyle("-fx-background-color: transparent;"); @@ -162,14 +190,14 @@ public class DecoratorSkin extends SkinBase { buttonsContainer.getChildren().setAll(btnMin, btnClose); } - titleContainer.setRight(buttonsContainer); + titleBar.setRight(buttonsContainer); } root.setTop(titleContainer); - getChildren().setAll(root); + getChildren().add(parent); } - private Node createNavBar(Decorator skinnable, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) { + private Node createNavBar(Decorator skinnable, boolean titleBarTransparent, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) { BorderPane navBar = new BorderPane(); { HBox navLeft = new HBox(); @@ -202,6 +230,13 @@ public class DecoratorSkin extends SkinBase { if (title != null) { Label titleLabel = new Label(); titleLabel.getStyleClass().add("jfx-decorator-title"); + if (titleBarTransparent) titleLabel.pseudoClassStateChanged(TRANSPARENT, true); + if (!canClose && !canBack) { + titleLabel.setAlignment(Pos.CENTER); + // 20: in this case width of navLeft is 10, so to make the text center aligned, + // we should have width 2 * 10 reduced + titleLabel.setPrefWidth(leftPaneWidth - 20); + } titleLabel.setText(title); center.setLeft(titleLabel); BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java index 5a1478c64..3acddadd3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java @@ -18,10 +18,7 @@ package org.jackhuang.hmcl.ui.decorator; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.*; import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Skin; @@ -31,10 +28,10 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.wizard.Refreshable; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - public abstract class DecoratorTransitionPage extends Control implements DecoratorPage { protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle("")); + private final DoubleProperty leftPaneWidth = new SimpleDoubleProperty(); + private final BooleanProperty titleBarTransparent = new SimpleBooleanProperty(false); private final BooleanProperty backable = new SimpleBooleanProperty(false); private final BooleanProperty refreshable = new SimpleBooleanProperty(false); private Node currentPage; @@ -60,11 +57,11 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat if (to instanceof DecoratorPage) { state.bind(Bindings.createObjectBinding(() -> { State state = ((DecoratorPage) to).stateProperty().get(); - return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true); + return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, titleBarTransparent.get(), leftPaneWidth.get()); }, ((DecoratorPage) to).stateProperty())); } else { state.unbind(); - state.set(new State("", null, backable.get(), false, true)); + state.set(new State("", null, backable.get(), false, true, titleBarTransparent.get(), leftPaneWidth.get())); } if (to instanceof Region) { @@ -112,4 +109,28 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); } + + public double getLeftPaneWidth() { + return leftPaneWidth.get(); + } + + public DoubleProperty leftPaneWidthProperty() { + return leftPaneWidth; + } + + public void setLeftPaneWidth(double leftPaneWidth) { + this.leftPaneWidth.set(leftPaneWidth); + } + + public boolean isTitleBarTransparent() { + return titleBarTransparent.get(); + } + + public BooleanProperty titleBarTransparentProperty() { + return titleBarTransparent; + } + + public void setTitleBarTransparent(boolean titleBarTransparent) { + this.titleBarTransparent.set(titleBarTransparent); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index a45edbabb..62c741537 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -59,7 +59,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.SINE; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class MainPage extends StackPane implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle("Hello Minecraft! Launcher " + Metadata.VERSION)); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle("HMCL " + Metadata.VERSION)); private final PopupMenu menu = new PopupMenu(); private final JFXPopup popup = new JFXPopup(menu); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 4e2aaee3c..445791453 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -78,6 +78,8 @@ public class RootPage extends DecoratorTabPage { private final TabHeader.Tab profileTab = new TabHeader.Tab("profile"); public RootPage() { + setLeftPaneWidth(200); + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); Profile profile = Profiles.getSelectedProfile(); @@ -104,6 +106,7 @@ public class RootPage extends DecoratorTabPage { @Override protected void onNavigated(Node to) { backableProperty().set(!(to instanceof MainPage)); + setTitleBarTransparent(to instanceof MainPage); super.onNavigated(to); } @@ -237,18 +240,8 @@ public class RootPage extends DecoratorTabPage { // the root page, with the sidebar in left, navigator in center. BorderPane root = new BorderPane(); - - { - StackPane drawerContainer = new StackPane(); - FXUtils.setLimitWidth(drawerContainer, 200); - drawerContainer.getStyleClass().add("gray-background"); - drawerContainer.getChildren().setAll(sideBar); - FXUtils.setOverflowHidden(drawerContainer, 8); - - StackPane wrapper = new StackPane(drawerContainer); - wrapper.setPadding(new Insets(4, 0, 4, 4)); - root.setLeft(wrapper); - } + sideBar.setPrefWidth(200); + root.setLeft(sideBar); { control.transitionPane.getStyleClass().add("jfx-decorator-content-container"); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index f2cdfdf7e..b9930efd3 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -134,7 +134,7 @@ } .two-line-list-item > .title { - -fx-text-fill: black; + -fx-text-fill: #292929; -fx-font-size: 15px; } @@ -153,10 +153,6 @@ -fx-text-fill: white; } -.window-title-bar .separator { - -fx-fill: -fx-base-darker-color; -} - .darker-fill { -fx-fill: -fx-base-darker-color; } @@ -373,11 +369,14 @@ .jfx-tool-bar { -fx-font-size: 13.0; -fx-font-weight: BOLD; - -fx-background-color: -fx-base-color; -fx-pref-width: 100.0%; -fx-pref-height: 32.0px; } +.jfx-tool-bar .background { + -fx-background-color: -fx-base-color; +} + .jfx-tool-bar HBox { -fx-alignment: center-left; -fx-padding: 0.0 5.0; @@ -1098,6 +1097,7 @@ } .jfx-decorator-drawer { + -fx-background-color: rgba(244, 244, 244, 0.55); } .jfx-decorator-title { @@ -1105,6 +1105,10 @@ -fx-font-size: 14; } +.jfx-decorator-title:transparent { + -fx-text-fill: black; +} + .jfx-decorator-tab .tab-container .tab-label { -fx-text-fill: -fx-base-disabled-text-fill; -fx-font-size: 14; @@ -1116,7 +1120,7 @@ .resize-border { -fx-border-color: -fx-base-color; - -fx-border-width: 0 2 2 2; + -fx-border-width: 2; } .debug-border {