alt: transparent title bar

This commit is contained in:
huanghongxun 2020-04-06 17:33:57 +08:00
parent d29a198f7f
commit c360a4abb0
9 changed files with 146 additions and 63 deletions

View File

@ -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());

View File

@ -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 <T> void playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> 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);

View File

@ -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);
});
}

View File

@ -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;
}
}
}

View File

@ -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<Decorator> {
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<Decorator> {
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<Decorator> {
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<Decorator> {
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<Decorator> {
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<Decorator> {
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<Decorator> {
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);

View File

@ -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> 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<State> 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);
}
}

View File

@ -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> state = new ReadOnlyObjectWrapper<>(State.fromTitle("Hello Minecraft! Launcher " + Metadata.VERSION));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle("HMCL " + Metadata.VERSION));
private final PopupMenu menu = new PopupMenu();
private final JFXPopup popup = new JFXPopup(menu);

View File

@ -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");

View File

@ -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 {