Refactor decorator as well as navigation

This commit is contained in:
huangyuhui
2018-09-02 21:53:34 +08:00
parent b8793f7575
commit 5fb0035b2f
27 changed files with 1381 additions and 786 deletions

View File

@@ -60,21 +60,11 @@ public class AdvancedListItem2 extends Control {
return onActionProperty().get(); return onActionProperty().get();
} }
private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() { private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
@Override @Override
protected void invalidated() { protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get()); setEventHandler(ActionEvent.ACTION, get());
} }
@Override
public Object getBean() {
return AdvancedListItem2.this;
}
@Override
public String getName() {
return "onAction";
}
}; };
@Override @Override

View File

@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox; import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.profile.ProfileList; import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameList; import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.FutureCallback;
@@ -53,7 +54,7 @@ public final class Controllers {
private static ProfileList profileListPage = null; private static ProfileList profileListPage = null;
private static AuthlibInjectorServersPage serversPage = null; private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController; private static LeftPaneController leftPaneController;
private static Decorator decorator; private static DecoratorController decorator;
public static Scene getScene() { public static Scene getScene() {
return scene; return scene;
@@ -106,7 +107,7 @@ public final class Controllers {
} }
// FXThread // FXThread
public static Decorator getDecorator() { public static DecoratorController getDecorator() {
return decorator; return decorator;
} }
@@ -125,20 +126,14 @@ public final class Controllers {
stage.setOnCloseRequest(e -> Launcher.stopApplication()); stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true); decorator = new DecoratorController(stage, getMainPage());
decorator.showPage(null); leftPaneController = new LeftPaneController();
leftPaneController = new LeftPaneController(decorator.getLeftPane()); decorator.getDecorator().drawerProperty().setAll(leftPaneController);
Task.of(JavaVersion::initialize).start(); Task.of(JavaVersion::initialize).start();
decorator.setCustomMaximize(false); scene = new Scene(decorator.getDecorator(), 800, 519);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().setAll(config().getTheme().getStylesheets()); scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
stage.setMaxHeight(521);
stage.getIcons().add(new Image("/assets/img/icon.png")); stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE); stage.setTitle(Metadata.TITLE);
@@ -189,10 +184,7 @@ public final class Controllers {
} }
public static void navigate(Node node) { public static void navigate(Node node) {
if (decorator.getNowPage() == node) decorator.getNavigator().navigate(node);
decorator.showPage(null);
else
decorator.showPage(node);
} }
public static boolean isStopped() { public static boolean isStopped() {

View File

@@ -1,733 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer {
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 static final SVGGlyph resizeMax = Lang.apply(new SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final SVGGlyph resizeMin = Lang.apply(new SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Decorator.class.getName() + ".dialog.closeListener";
private final ObjectProperty<Runnable> onCloseButtonAction;
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private final Node mainPage;
private final boolean max, min;
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, maximized;
private BoundingBox originalBox, maximizedBox;
private final TransitionHandler animationHandler;
private JFXDialog dialog;
private StackContainerPane dialogPane;
@FXML
private StackPane contentPlaceHolder;
@FXML
private StackPane drawerWrapper;
@FXML
private BorderPane titleContainer;
@FXML
private BorderPane leftRootPane;
@FXML
private HBox buttonsContainer;
@FXML
private JFXButton backNavButton;
@FXML
private JFXButton refreshNavButton;
@FXML
private JFXButton closeNavButton;
@FXML
private JFXButton refreshMenuButton;
@FXML
private Label titleLabel;
@FXML
private Label lblTitle;
@FXML
private AdvancedListBox leftPane;
@FXML
private JFXDrawer drawer;
@FXML
private StackPane titleBurgerContainer;
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@FXML
private JFXButton btnClose;
@FXML
private HBox navLeft;
@FXML
private ImageView welcomeView;
@FXML
private Rectangle separator;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
}
public Decorator(Stage primaryStage, Node mainPage, String title, boolean max, boolean min) {
this.primaryStage = primaryStage;
this.mainPage = mainPage;
this.max = max;
this.min = min;
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication);
primaryStage.initStyle(StageStyle.UNDECORATED);
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
close.fillProperty().bind(Theme.foregroundFillBinding());
minus.fillProperty().bind(Theme.foregroundFillBinding());
resizeMax.fillProperty().bind(Theme.foregroundFillBinding());
resizeMin.fillProperty().bind(Theme.foregroundFillBinding());
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), 15, 15));
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15));
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15));
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
lblTitle.setText(title);
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getClickCount() == 2)
btnMax.fire();
});
welcomeView.setCursor(Cursor.HAND);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> drawerWrapper.getChildren().remove(welcomeView))
);
nowAnimation.play();
});
if (!ConfigHolder.isNewlyCreated() || config().getLocalization().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
//JFXDepthManager.setDepth(titleContainer, 1);
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging) allowMove = false;
});
Rectangle rectangle = new Rectangle(0, 0, 0, 0);
rectangle.widthProperty().bind(titleContainer.widthProperty());
rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty()));
titleContainer.setClip(rectangle);
animationHandler = new TransitionHandler(contentPlaceHolder);
setupBackground();
setupAuthlibInjectorDnD();
}
// ==== Background ====
private void setupBackground() {
drawerWrapper.backgroundProperty().bind(
Bindings.createObjectBinding(
() -> {
Image image = null;
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
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)));
},
config().backgroundImageTypeProperty(),
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg
*/
private Image loadDefaultBackgroundImage() {
Optional<Image> image = randomImageIn(Paths.get("bg"));
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.png"));
}
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.jpg"));
}
return image.orElse(defaultBackground);
}
private Optional<Image> randomImageIn(Path imageDir) {
if (!Files.isDirectory(imageDir)) {
return Optional.empty();
}
List<Path> candidates;
try {
candidates = Files.list(imageDir)
.filter(Files::isRegularFile)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
})
.collect(toList());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to list files in ./bg", e);
return Optional.empty();
}
Random rnd = new Random();
while (candidates.size() > 0) {
int selected = rnd.nextInt(candidates.size());
Optional<Image> loaded = tryLoadImage(candidates.get(selected));
if (loaded.isPresent()) {
return Optional.of(loaded.get());
} else {
candidates.remove(selected);
}
}
return Optional.empty();
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
}
return Optional.empty();
}
private boolean isMaximized() {
switch (OperatingSystem.CURRENT_OS) {
case OSX:
Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
return primaryStage.getWidth() >= bounds.getWidth() && primaryStage.getHeight() >= bounds.getHeight();
default:
return primaryStage.isMaximized();
}
}
// ====
@FXML
private void onMouseMoved(MouseEvent mouseEvent) {
if (!isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = getBoundsInParent();
if (getBorder() != null && getBorder().getStrokes().size() > 0) {
double borderWidth = this.contentPlaceHolder.snappedLeftInset();
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NE_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SE_RESIZE);
} else {
setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NW_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SW_RESIZE);
} else {
setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
setCursor(Cursor.N_RESIZE);
} else if (this.isBottomEdge(x, y, boundsInParent)) {
setCursor(Cursor.S_RESIZE);
} else {
setCursor(Cursor.DEFAULT);
}
this.updateInitMouseValues(mouseEvent);
}
}
} else {
setCursor(Cursor.DEFAULT);
}
}
@FXML
private void onMouseReleased() {
isDragging = false;
}
@FXML
private void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
double deltaY = this.newY - this.initY;
Cursor cursor = this.getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (this.allowMove) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
}
}
}
}
@FXML
private void onMin() {
primaryStage.setIconified(true);
}
@FXML
private void onMax() {
if (!max) return;
if (!this.isCustomMaximize()) {
this.primaryStage.setMaximized(!this.primaryStage.isMaximized());
this.maximized = this.primaryStage.isMaximized();
if (this.primaryStage.isMaximized()) {
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
} else {
if (!this.maximized) {
this.originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight());
Screen screen = Screen.getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()).get(0);
Rectangle2D bounds = screen.getVisualBounds();
this.maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
primaryStage.setX(this.maximizedBox.getMinX());
primaryStage.setY(this.maximizedBox.getMinY());
primaryStage.setWidth(this.maximizedBox.getWidth());
primaryStage.setHeight(this.maximizedBox.getHeight());
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
primaryStage.setX(this.originalBox.getMinX());
primaryStage.setY(this.originalBox.getMinY());
primaryStage.setWidth(this.originalBox.getWidth());
primaryStage.setHeight(this.originalBox.getHeight());
this.originalBox = null;
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
this.maximized = !this.maximized;
}
}
@FXML
private void onClose() {
onCloseButtonAction.get().run();
}
private void updateInitMouseValues(MouseEvent mouseEvent) {
initX = mouseEvent.getScreenX();
initY = mouseEvent.getScreenY();
xOffset = mouseEvent.getSceneX();
yOffset = mouseEvent.getSceneY();
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < getWidth() && x > getWidth() - contentPlaceHolder.snappedLeftInset();
}
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
}
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
return y < getHeight() && y > getHeight() - contentPlaceHolder.snappedLeftInset();
}
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
}
private boolean setStageWidth(double width) {
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
primaryStage.setWidth(width);
initX = newX;
return true;
} else {
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
primaryStage.setWidth(titleContainer.getMinWidth());
return false;
}
}
private boolean setStageHeight(double height) {
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
primaryStage.setHeight(height);
initY = newY;
return true;
} else {
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
primaryStage.setHeight(titleContainer.getHeight());
return false;
}
}
public void setMaximized(boolean maximized) {
if (this.maximized != maximized) {
Platform.runLater(btnMax::fire);
}
}
private void showCloseNavButton() {
navLeft.getChildren().add(closeNavButton);
}
private void hideCloseNavButton() {
navLeft.getChildren().remove(closeNavButton);
}
private void setContent(Node content, AnimationProducer animation) {
isWizardPageNow = false;
animationHandler.setContent(content, animation);
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content, true);
}
refreshNavButton.setVisible(content instanceof Refreshable);
backNavButton.setVisible(content != mainPage);
String prefix = category == null ? "" : category + " - ";
titleLabel.textProperty().unbind();
if (content instanceof WizardPage)
titleLabel.setText(prefix + ((WizardPage) content).getTitle());
if (content instanceof DecoratorPage)
titleLabel.textProperty().bind(((DecoratorPage) content).titleProperty());
}
private String category;
private Node nowPage;
private boolean isWizardPageNow;
public Node getNowPage() {
return nowPage;
}
public void showPage(Node content) {
FXUtils.checkFxUserThread();
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
if (content != null)
contentPlaceHolder.getStyleClass().add("gray-background");
Node c = content == null ? mainPage : content;
onEnd();
if (nowPage instanceof DecoratorPage)
((DecoratorPage) nowPage).onClose();
nowPage = content;
setContent(c, ContainerAnimations.FADE.getAnimationProducer());
if (c instanceof Region) {
// Let root pane fix window size.
StackPane parent = (StackPane) c.getParent();
((Region) c).prefWidthProperty().bind(parent.widthProperty());
((Region) c).prefHeightProperty().bind(parent.heightProperty());
}
}
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialog == null) {
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
dialog.setDialogContainer(drawerWrapper);
dialog.setOverlayClose(false);
dialog.show();
}
dialogPane.push(node);
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
}
@SuppressWarnings("unchecked")
private void closeDialog(Node node) {
FXUtils.checkFxUserThread();
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
if (dialog != null) {
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
}
public void startWizard(WizardProvider wizardProvider) {
startWizard(wizardProvider, null);
}
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
}
@Override
public void onStart() {
backNavButton.setVisible(true);
backNavButton.setDisable(false);
showCloseNavButton();
refreshNavButton.setVisible(false);
}
@Override
public void onEnd() {
backNavButton.setVisible(false);
hideCloseNavButton();
refreshNavButton.setVisible(false);
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
contentPlaceHolder.getStyleClass().add("white-background");
setContent(page, nav.getAnimation().getAnimationProducer());
isWizardPageNow = true;
}
@FXML
private void onRefresh() {
((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh();
}
@FXML
private void onCloseNav() {
wizardController.onCancel();
showPage(null);
}
@FXML
private void onBack() {
if (isWizardPageNow && wizardController.canPrev())
wizardController.onPrev(true);
else
onCloseNav();
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public boolean isCustomMaximize() {
return customMaximize.get();
}
public BooleanProperty customMaximizeProperty() {
return customMaximize;
}
public void setCustomMaximize(boolean customMaximize) {
this.customMaximize.set(customMaximize);
}
@Override
public WizardController getWizardController() {
return wizardController;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
private void setupAuthlibInjectorDnD() {
addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler());
addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler(
url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url))));
}
}

View File

@@ -43,9 +43,10 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController { public final class LeftPaneController extends AdvancedListBox {
public LeftPaneController() {
public LeftPaneController(AdvancedListBox leftPane) {
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem(); AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage())); accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
@@ -67,10 +68,10 @@ public final class LeftPaneController {
.then(Color.RED) .then(Color.RED)
.otherwise(Color.BLACK)); .otherwise(Color.BLACK));
launcherSettingsItem.maxWidthProperty().bind(leftPane.widthProperty()); launcherSettingsItem.maxWidthProperty().bind(widthProperty());
launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
leftPane this
.startCategory(i18n("account").toUpperCase()) .startCategory(i18n("account").toUpperCase())
.add(accountListItem) .add(accountListItem)
.startCategory(i18n("version").toUpperCase()) .startCategory(i18n("version").toUpperCase())

View File

@@ -23,18 +23,17 @@ import javafx.fxml.FXML;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage { public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page")); private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page"));
{ {
FXUtils.loadFXML(this, "/assets/fxml/main.fxml"); FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
} }
@FXML @FXML

View File

@@ -36,7 +36,7 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font; import javafx.scene.text.Font;
import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.UpdateChannel; import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.upgrade.UpdateChecker;

View File

@@ -28,8 +28,8 @@ import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import java.io.File; import java.io.File;

View File

@@ -25,7 +25,7 @@ import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList; import org.jackhuang.hmcl.util.MappedObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;

View File

@@ -23,7 +23,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList; import org.jackhuang.hmcl.util.MappedObservableList;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;

View File

@@ -32,7 +32,7 @@ import javafx.scene.layout.Region;
*/ */
public class DialogCloseEvent extends Event { public class DialogCloseEvent extends Event {
public static final EventType<DialogCloseEvent> CLOSE = new EventType<>("CLOSE"); public static final EventType<DialogCloseEvent> CLOSE = new EventType<>("DIALOG_CLOSE");
public DialogCloseEvent() { public DialogCloseEvent() {
super(CLOSE); super(CLOSE);

View File

@@ -0,0 +1,155 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import java.util.Optional;
import java.util.Stack;
public class Navigator extends StackPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
private final Stack<Node> stack = new Stack<>();
private final TransitionHandler animationHandler = new TransitionHandler(this);
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper();
public Navigator(Node init) {
stack.push(init);
getChildren().setAll(init);
}
public void navigate(Node node) {
FXUtils.checkFxUserThread();
Node from = stack.peek();
if (from == node)
return;
stack.push(node);
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
setContent(node);
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
EventHandler<PageCloseEvent> handler = event -> close(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(PageCloseEvent.CLOSE, handler);
}
public void close() {
close(stack.peek());
}
@SuppressWarnings("unchecked")
public void close(Node from) {
FXUtils.checkFxUserThread();
stack.remove(from);
Node node = stack.peek();
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
setContent(node);
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> from.removeEventHandler(PageCloseEvent.CLOSE, (EventHandler<PageCloseEvent>) handler));
}
public Node getCurrentPage() {
return stack.peek();
}
public boolean canGoBack() {
return stack.size() > 1;
}
private void setContent(Node content) {
animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer());
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content, true);
}
}
public EventHandler<NavigationEvent> getOnNavigated() {
return onNavigated.get();
}
public ObjectProperty<EventHandler<NavigationEvent>> onNavigatedProperty() {
return onNavigated;
}
public void setOnNavigated(EventHandler<NavigationEvent> onNavigated) {
this.onNavigated.set(onNavigated);
}
private ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") {
@Override
protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATED, get());
}
};
public EventHandler<NavigationEvent> getOnNavigating() {
return onNavigating.get();
}
public ObjectProperty<EventHandler<NavigationEvent>> onNavigatingProperty() {
return onNavigating;
}
public void setOnNavigating(EventHandler<NavigationEvent> onNavigating) {
this.onNavigating.set(onNavigating);
}
private ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") {
@Override
protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATING, get());
}
};
public static class NavigationEvent extends Event {
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
private final Node node;
public NavigationEvent(Object source, Node target, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.node = target;
}
public Node getNode() {
return node;
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;
/**
* Indicates a close operation on the navigator page.
*
* @author huangyuhui
*/
public class PageCloseEvent extends Event {
public static final EventType<PageCloseEvent> CLOSE = new EventType<>("PAGE_CLOSE");
public PageCloseEvent() {
super(CLOSE);
}
public PageCloseEvent(Object source, EventTarget target) {
super(source, target, CLOSE);
}
}

View File

@@ -0,0 +1,201 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.layout.Background;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class DecoratorControl extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
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<>();
private final StringProperty title = new SimpleStringProperty();
private final StringProperty drawerTitle = new SimpleStringProperty();
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onRefreshNavButtonAction = new SimpleObjectProperty<>();
private final BooleanProperty closeNavButtonVisible = new SimpleBooleanProperty(true);
private final BooleanProperty canRefresh = new SimpleBooleanProperty(false);
private final BooleanProperty canBack = new SimpleBooleanProperty(false);
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private StackPane drawerWrapper;
public DecoratorControl(Stage primaryStage) {
this.primaryStage = primaryStage;
primaryStage.initStyle(StageStyle.UNDECORATED);
}
public Stage getPrimaryStage() {
return primaryStage;
}
public StackPane getDrawerWrapper() {
return drawerWrapper;
}
void setDrawerWrapper(StackPane drawerWrapper) {
this.drawerWrapper = drawerWrapper;
}
public ObservableList<Node> getDrawer() {
return drawer.get();
}
public ListProperty<Node> drawerProperty() {
return drawer;
}
public void setDrawer(ObservableList<Node> drawer) {
this.drawer.set(drawer);
}
public ObservableList<Node> getContent() {
return content.get();
}
public ListProperty<Node> contentProperty() {
return content;
}
public void setContent(ObservableList<Node> content) {
this.content.set(content);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getDrawerTitle() {
return drawerTitle.get();
}
public StringProperty drawerTitleProperty() {
return drawerTitle;
}
public void setDrawerTitle(String drawerTitle) {
this.drawerTitle.set(drawerTitle);
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public boolean isCloseNavButtonVisible() {
return closeNavButtonVisible.get();
}
public BooleanProperty closeNavButtonVisibleProperty() {
return closeNavButtonVisible;
}
public void setCloseNavButtonVisible(boolean closeNavButtonVisible) {
this.closeNavButtonVisible.set(closeNavButtonVisible);
}
public ObservableList<Node> getContainer() {
return container.get();
}
public ListProperty<Node> containerProperty() {
return container;
}
public void setContainer(ObservableList<Node> container) {
this.container.set(container);
}
public Background getContentBackground() {
return contentBackground.get();
}
public ObjectProperty<Background> contentBackgroundProperty() {
return contentBackground;
}
public void setContentBackground(Background contentBackground) {
this.contentBackground.set(contentBackground);
}
public BooleanProperty canRefreshProperty() {
return canRefresh;
}
public BooleanProperty canBackProperty() {
return canBack;
}
public BooleanProperty canCloseProperty() {
return canClose;
}
public ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonActionProperty() {
return onBackNavButtonAction;
}
public ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonActionProperty() {
return onCloseNavButtonAction;
}
public ObjectProperty<EventHandler<ActionEvent>> onRefreshNavButtonActionProperty() {
return onRefreshNavButtonAction;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DecoratorSkin(this);
}
public void minimize() {
primaryStage.setIconified(true);
}
public void close() {
onCloseButtonAction.get().run();
}
}

View File

@@ -0,0 +1,374 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.FutureCallback;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.function.Consumer;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final DecoratorControl decorator;
private final ImageView welcomeView;
private final Navigator navigator;
private final Node mainPage;
private JFXDialog dialog;
private StackContainerPane dialogPane;
public DecoratorController(Stage stage, Node mainPage) {
this.mainPage = mainPage;
decorator = new DecoratorControl(stage);
decorator.titleProperty().set(Metadata.TITLE);
decorator.setOnCloseButtonAction(Launcher::stopApplication);
navigator = new Navigator(mainPage);
navigator.setOnNavigating(this::onNavigating);
navigator.setOnNavigated(this::onNavigated);
decorator.getContent().setAll(navigator);
decorator.onCloseNavButtonActionProperty().set(e -> close());
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(new Image("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView))
);
nowAnimation.play();
});
if (ConfigHolder.isNewlyCreated() && config().getLocalization().getLocale() == Locale.CHINA)
decorator.getContainer().setAll(welcomeView);
setupBackground();
setupAuthlibInjectorDnD();
}
public DecoratorControl getDecorator() {
return decorator;
}
// ==== Background ====
private void setupBackground() {
decorator.backgroundProperty().bind(
Bindings.createObjectBinding(
() -> {
Image image = null;
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
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)));
},
config().backgroundImageTypeProperty(),
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg
*/
private Image loadDefaultBackgroundImage() {
Optional<Image> image = randomImageIn(Paths.get("bg"));
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.png"));
}
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.jpg"));
}
return image.orElse(defaultBackground);
}
private Optional<Image> randomImageIn(Path imageDir) {
if (!Files.isDirectory(imageDir)) {
return Optional.empty();
}
List<Path> candidates;
try {
candidates = Files.list(imageDir)
.filter(Files::isRegularFile)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
})
.collect(toList());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to list files in ./bg", e);
return Optional.empty();
}
Random rnd = new Random();
while (candidates.size() > 0) {
int selected = rnd.nextInt(candidates.size());
Optional<Image> loaded = tryLoadImage(candidates.get(selected));
if (loaded.isPresent()) {
return loaded;
} else {
candidates.remove(selected);
}
}
return Optional.empty();
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
}
return Optional.empty();
}
// ==== Navigation ====
public Navigator getNavigator() {
return navigator;
}
private void close() {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
page.onForceToClose();
} else {
navigator.close();
}
}
private void back() {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
if (page.onClose())
navigator.close();
} else {
navigator.close();
}
}
private void refresh() {
if (navigator.getCurrentPage() instanceof Refreshable) {
Refreshable refreshable = (Refreshable) navigator.getCurrentPage();
if (refreshable.canRefreshProperty().get())
refreshable.refresh();
}
}
private void onNavigating(Navigator.NavigationEvent event) {
Node from = event.getNode();
if (from instanceof DecoratorPage)
((DecoratorPage) from).onClose();
}
private void onNavigated(Navigator.NavigationEvent event) {
Node to = event.getNode();
if (to instanceof Refreshable) {
decorator.canRefreshProperty().bind(((Refreshable) to).canRefreshProperty());
} else {
decorator.canRefreshProperty().unbind();
decorator.canRefreshProperty().set(false);
}
if (to instanceof DecoratorPage) {
decorator.drawerTitleProperty().bind(((DecoratorPage) to).titleProperty());
decorator.canCloseProperty().set(((DecoratorPage) to).canForceToClose());
} else {
decorator.drawerTitleProperty().unbind();
decorator.drawerTitleProperty().set("");
decorator.canCloseProperty().set(false);
}
decorator.canBackProperty().set(navigator.canGoBack());
if (navigator.canGoBack()) {
decorator.setContentBackground(new Background(new BackgroundFill(Color.rgb(244, 244, 244, 0.5), CornerRadii.EMPTY, Insets.EMPTY)));
} else {
decorator.setContentBackground(null);
}
if (to instanceof Region) {
Region region = (Region) to;
// Let root pane fix window size.
StackPane parent = (StackPane) region.getParent();
region.prefWidthProperty().bind(parent.widthProperty());
region.prefHeightProperty().bind(parent.heightProperty());
}
}
// ==== Dialog ====
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialog == null) {
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
dialog.setDialogContainer(decorator.getDrawerWrapper());
dialog.setOverlayClose(false);
dialog.show();
}
dialogPane.push(node);
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
}
@SuppressWarnings("unchecked")
private void closeDialog(Node node) {
FXUtils.checkFxUserThread();
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
if (dialog != null) {
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
}
public void showDialog(String text) {
showDialog(text, null);
}
public void showDialog(String text, String title) {
showDialog(text, title, MessageBox.INFORMATION_MESSAGE);
}
public void showDialog(String text, String title, int type) {
showDialog(text, title, type, null);
}
public void showDialog(String text, String title, int type, Runnable onAccept) {
showDialog(new MessageDialogPane(text, title, type, onAccept));
}
public void showConfirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
showDialog(new MessageDialogPane(text, title, onAccept, onCancel));
}
public InputDialogPane showInputDialog(String text, FutureCallback<String> onResult) {
InputDialogPane pane = new InputDialogPane(text, onResult);
showDialog(pane);
return pane;
}
public Region showTaskDialog(TaskExecutor executor, String title, String subtitle) {
return showTaskDialog(executor, title, subtitle, null);
}
public Region showTaskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
showDialog(pane);
return pane;
}
// ==== Wizard ====
public void startWizard(WizardProvider wizardProvider) {
startWizard(wizardProvider, null);
}
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category));
}
// ==== Authlib Injector DnD ====
private void setupAuthlibInjectorDnD() {
decorator.addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler());
decorator.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler(
url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url))));
}
}

View File

@@ -15,13 +15,21 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * along with this program. If not, see {http://www.gnu.org/licenses/}.
*/ */
package org.jackhuang.hmcl.ui.wizard; package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
public interface DecoratorPage { public interface DecoratorPage {
StringProperty titleProperty(); StringProperty titleProperty();
default void onClose() { default boolean canForceToClose() {
return false;
}
default boolean onClose() {
return true;
}
default void onForceToClose() {
} }
} }

View File

@@ -0,0 +1,422 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<DecoratorControl> {
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 titleContainer;
private final StackPane contentPlaceHolder;
private final JFXButton refreshNavButton;
private final JFXButton closeNavButton;
private final HBox navLeft;
private final Stage primaryStage;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging;
private BoundingBox originalBox, maximizedBox;
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
public DecoratorSkin(DecoratorControl control) {
super(control);
primaryStage = control.getPrimaryStage();
minus.fillProperty().bind(Theme.foregroundFillBinding());
DecoratorControl skinnable = getSkinnable();
BorderPane root = new BorderPane();
root.getStyleClass().setAll("jfx-decorator", "resize-border");
root.setMaxHeight(519);
root.setMaxWidth(800);
StackPane drawerWrapper = new StackPane();
skinnable.setDrawerWrapper(drawerWrapper);
drawerWrapper.getStyleClass().setAll("jfx-decorator-drawer");
drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty());
FXUtils.setOverflowHidden(drawerWrapper, true);
{
BorderPane drawer = new BorderPane();
{
BorderPane leftRootPane = new BorderPane();
FXUtils.setLimitWidth(leftRootPane, 200);
leftRootPane.getStyleClass().setAll("jfx-decorator-content-container");
StackPane drawerContainer = new StackPane();
drawerContainer.getStyleClass().setAll("gray-background");
Bindings.bindContent(drawerContainer.getChildren(), skinnable.drawerProperty());
leftRootPane.setCenter(drawerContainer);
Rectangle separator = new Rectangle();
separator.heightProperty().bind(drawer.heightProperty());
separator.setWidth(1);
separator.setFill(Color.GRAY);
leftRootPane.setRight(separator);
drawer.setLeft(leftRootPane);
}
{
contentPlaceHolder = new StackPane();
contentPlaceHolder.getStyleClass().setAll("jfx-decorator-content-container");
contentPlaceHolder.backgroundProperty().bind(skinnable.contentBackgroundProperty());
FXUtils.setOverflowHidden(contentPlaceHolder, true);
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
drawer.setCenter(contentPlaceHolder);
}
drawerWrapper.getChildren().add(drawer);
}
{
StackPane container = new StackPane();
Bindings.bindContent(container.getChildren(), skinnable.containerProperty());
ListChangeListener<Node> listener = new ListChangeListener<Node>() {
@Override
public void onChanged(Change<? extends Node> c) {
if (skinnable.getContainer().isEmpty()) {
container.setMouseTransparent(true);
container.setVisible(false);
} else {
container.setMouseTransparent(false);
container.setVisible(true);
}
}
};
skinnable.containerProperty().addListener(listener);
listener.onChanged(null);
drawerWrapper.getChildren().add(container);
}
root.setCenter(drawerWrapper);
titleContainer = new BorderPane();
titleContainer.setOnMouseReleased(this::onMouseReleased);
titleContainer.setOnMouseDragged(this::onMouseDragged);
titleContainer.setOnMouseMoved(this::onMouseMoved);
titleContainer.setPickOnBounds(false);
titleContainer.setMinHeight(40);
titleContainer.getStyleClass().setAll("jfx-tool-bar");
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging) allowMove = false;
});
Rectangle rectangle = new Rectangle(0, 0, 0, 0);
rectangle.widthProperty().bind(titleContainer.widthProperty());
rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty()));
titleContainer.setClip(rectangle);
{
BorderPane titleWrapper = new BorderPane();
FXUtils.setLimitWidth(titleWrapper, 200);
{
Label lblTitle = new Label();
BorderPane.setMargin(lblTitle, new Insets(0, 0, 0, 3));
lblTitle.setStyle("-fx-background-color: transparent; -fx-text-fill: -fx-base-text-fill; -fx-font-size: 15px;");
lblTitle.setMouseTransparent(true);
lblTitle.textProperty().bind(skinnable.titleProperty());
BorderPane.setAlignment(lblTitle, Pos.CENTER);
titleWrapper.setCenter(lblTitle);
Rectangle separator = new Rectangle();
separator.heightProperty().bind(titleWrapper.heightProperty());
separator.setWidth(1);
separator.setFill(Color.GRAY);
titleWrapper.setRight(separator);
}
titleContainer.setLeft(titleWrapper);
BorderPane navBar = new BorderPane();
{
navLeft = new HBox();
navLeft.setAlignment(Pos.CENTER_LEFT);
navLeft.setPadding(new Insets(0, 5, 0, 5));
{
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().setAll("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().bind(skinnable.canBackProperty());
closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().setAll("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
navLeft.getChildren().setAll(backNavButton);
skinnable.canCloseProperty().addListener((a, b, newValue) -> {
if (newValue) navLeft.getChildren().setAll(backNavButton, closeNavButton);
else navLeft.getChildren().setAll(backNavButton);
});
}
navBar.setLeft(navLeft);
VBox navCenter = new VBox();
navCenter.setAlignment(Pos.CENTER_LEFT);
Label titleLabel = new Label();
titleLabel.getStyleClass().setAll("jfx-decorator-title");
titleLabel.textProperty().bind(skinnable.drawerTitleProperty());
navCenter.getChildren().setAll(titleLabel);
navBar.setCenter(navCenter);
HBox navRight = new HBox();
navRight.setAlignment(Pos.CENTER_RIGHT);
refreshNavButton = new JFXButton();
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1));
refreshNavButton.getStyleClass().setAll("jfx-decorator-button");
refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
refreshNavButton.visibleProperty().bind(skinnable.canRefreshProperty());
navRight.getChildren().setAll(refreshNavButton);
navBar.setRight(navRight);
}
titleContainer.setCenter(navBar);
HBox buttonsContainer = new HBox();
buttonsContainer.setStyle("-fx-background-color: transparent;");
buttonsContainer.setAlignment(Pos.CENTER_RIGHT);
buttonsContainer.setPadding(new Insets(4));
{
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
separator.heightProperty().bind(navBar.heightProperty());
JFXButton btnMin = new JFXButton();
StackPane pane = new StackPane(minus);
pane.setAlignment(Pos.CENTER);
btnMin.setGraphic(pane);
btnMin.getStyleClass().setAll("jfx-decorator-button");
btnMin.setOnAction(e -> skinnable.minimize());
JFXButton btnClose = new JFXButton();
btnClose.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
btnClose.getStyleClass().setAll("jfx-decorator-button");
btnClose.setOnAction(e -> skinnable.close());
buttonsContainer.getChildren().setAll(separator, btnMin, btnClose);
}
titleContainer.setRight(buttonsContainer);
}
root.setTop(titleContainer);
getChildren().setAll(root);
getSkinnable().closeNavButtonVisibleProperty().addListener((a, b, newValue) -> {
if (newValue) navLeft.getChildren().add(closeNavButton);
else navLeft.getChildren().remove(closeNavButton);
});
}
private void updateInitMouseValues(MouseEvent mouseEvent) {
initX = mouseEvent.getScreenX();
initY = mouseEvent.getScreenY();
xOffset = mouseEvent.getSceneX();
yOffset = mouseEvent.getSceneY();
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < getSkinnable().getWidth() && x > getSkinnable().getWidth() - contentPlaceHolder.snappedLeftInset();
}
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
}
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
return y < getSkinnable().getHeight() && y > getSkinnable().getHeight() - contentPlaceHolder.snappedLeftInset();
}
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
}
private boolean setStageWidth(double width) {
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
primaryStage.setWidth(width);
initX = newX;
return true;
} else {
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
primaryStage.setWidth(titleContainer.getMinWidth());
return false;
}
}
private boolean setStageHeight(double height) {
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
primaryStage.setHeight(height);
initY = newY;
return true;
} else {
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
primaryStage.setHeight(titleContainer.getHeight());
return false;
}
}
// ====
protected void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isFullScreen()) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = getSkinnable().getBoundsInParent();
if (getSkinnable().getBorder() != null && getSkinnable().getBorder().getStrokes().size() > 0) {
double borderWidth = contentPlaceHolder.snappedLeftInset();
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
getSkinnable().setCursor(Cursor.NE_RESIZE);
} else if (y > getSkinnable().getHeight() - borderWidth) {
getSkinnable().setCursor(Cursor.SE_RESIZE);
} else {
getSkinnable().setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
getSkinnable().setCursor(Cursor.NW_RESIZE);
} else if (y > getSkinnable().getHeight() - borderWidth) {
getSkinnable().setCursor(Cursor.SW_RESIZE);
} else {
getSkinnable().setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
getSkinnable().setCursor(Cursor.N_RESIZE);
} else if (this.isBottomEdge(x, y, boundsInParent)) {
getSkinnable().setCursor(Cursor.S_RESIZE);
} else {
getSkinnable().setCursor(Cursor.DEFAULT);
}
this.updateInitMouseValues(mouseEvent);
}
}
} else {
getSkinnable().setCursor(Cursor.DEFAULT);
}
}
protected void onMouseReleased(MouseEvent mouseEvent) {
isDragging = false;
}
protected void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress()) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
double deltaY = this.newY - this.initY;
Cursor cursor = getSkinnable().getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (this.allowMove) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
import org.jackhuang.hmcl.ui.wizard.*;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorDialogWizardDisplayer, Refreshable, DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final BooleanProperty canRefresh = new SimpleBooleanProperty();
private final TransitionHandler transitionHandler = new TransitionHandler(this);
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private final String category;
private Node nowPage;
public DecoratorWizardDisplayer(WizardProvider provider) {
this(provider, null);
}
public DecoratorWizardDisplayer(WizardProvider provider, String category) {
this.category = category;
wizardController.setProvider(provider);
wizardController.onStart();
getStyleClass().setAll("white-background");
}
@Override
public StringProperty titleProperty() {
return title;
}
@Override
public BooleanProperty canRefreshProperty() {
return canRefresh;
}
@Override
public WizardController getWizardController() {
return wizardController;
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
@Override
public void onStart() {
}
@Override
public void onEnd() {
fireEvent(new PageCloseEvent());
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
nowPage = page;
transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer());
canRefresh.set(page instanceof Refreshable);
String prefix = category == null ? "" : category + " - ";
if (page instanceof WizardPage)
title.set(prefix + ((WizardPage) page).getTitle());
}
@Override
public boolean canForceToClose() {
return true;
}
@Override
public void onForceToClose() {
wizardController.onCancel();
}
@Override
public boolean onClose() {
if (wizardController.canPrev()) {
wizardController.onPrev(true);
return false;
} else
return true;
}
@Override
public void refresh() {
((Refreshable) nowPage).refresh();
}
}

View File

@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -53,7 +54,8 @@ public final class DownloadWizardProvider implements WizardProvider {
private Task finishVersionDownloadingAsync(Map<String, Object> settings) { private Task finishVersionDownloadingAsync(Map<String, Object> settings) {
GameBuilder builder = profile.getDependency().gameBuilder(); GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name")); String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion()); builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
if (settings.containsKey("forge")) if (settings.containsKey("forge"))
@@ -65,7 +67,8 @@ public final class DownloadWizardProvider implements WizardProvider {
if (settings.containsKey("optifine")) if (settings.containsKey("optifine"))
builder.version((RemoteVersion) settings.get("optifine")); builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions()); return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions())
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
} }
private Task finishModpackInstallingAsync(Map<String, Object> settings) { private Task finishModpackInstallingAsync(Map<String, Object> settings) {
@@ -77,7 +80,8 @@ public final class DownloadWizardProvider implements WizardProvider {
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null); String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
if (selected == null || modpack == null || name == null) return null; if (selected == null || modpack == null || name == null) return null;
return ModpackHelper.getInstallTask(profile, selected, name, modpack); return ModpackHelper.getInstallTask(profile, selected, name, modpack)
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
} }
@Override @Override

View File

@@ -25,7 +25,7 @@ import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList; import org.jackhuang.hmcl.util.MappedObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;

View File

@@ -27,11 +27,10 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.FileItem; import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

View File

@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider; import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.VersionNumber; import org.jackhuang.hmcl.util.VersionNumber;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
@@ -78,6 +78,13 @@ public class GameList extends Control implements DecoratorPage {
loading.set(false); loading.set(false);
items.setAll(children); items.setAll(children);
children.forEach(GameListItem::checkSelection); children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener((a, b, newValue) -> {
toggleGroup.getToggles().stream()
.filter(it -> ((GameListItem) it.getUserData()).getVersion().equals(newValue))
.findFirst()
.ifPresent(it -> it.setSelected(true));
});
} }
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
GameListItem model = (GameListItem) toggle.getUserData(); GameListItem model = (GameListItem) toggle.getUserData();

View File

@@ -127,7 +127,7 @@ public class GameListItem extends Control {
public void modifyGameSettings() { public void modifyGameSettings() {
Controllers.getVersionPage().load(version, profile); Controllers.getVersionPage().load(version, profile);
Controllers.getDecorator().showPage(Controllers.getVersionPage()); Controllers.navigate(Controllers.getVersionPage());
} }
public void generateLaunchScript() { public void generateLaunchScript() {

View File

@@ -17,6 +17,13 @@
*/ */
package org.jackhuang.hmcl.ui.wizard; package org.jackhuang.hmcl.ui.wizard;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public interface Refreshable { public interface Refreshable {
void refresh(); void refresh();
default BooleanProperty canRefreshProperty() {
return new SimpleBooleanProperty(false);
}
} }

View File

@@ -1062,6 +1062,10 @@
.jfx-decorator-drawer { .jfx-decorator-drawer {
} }
.jfx-decorator-title {
-fx-text-fill: -fx-base-text-fill; -fx-font-size: 15;
}
.resize-border { .resize-border {
-fx-border-color: -fx-base-color; -fx-border-color: -fx-base-color;
-fx-border-width: 0 2 2 2; -fx-border-width: 0 2 2 2;

View File

@@ -22,7 +22,7 @@
maxWidth="800"> maxWidth="800">
<center> <center>
<StackPane fx:id="drawerWrapper" styleClass="jfx-decorator-drawer" FXUtils.overflowHidden="true"> <StackPane fx:id="drawerWrapper" styleClass="jfx-decorator-drawer" FXUtils.overflowHidden="true">
<BorderPane> <BorderPane fx:id="drawer">
<left> <left>
<StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container"> <StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container">
<BorderPane fx:id="leftRootPane"> <BorderPane fx:id="leftRootPane">

View File

@@ -2,9 +2,7 @@
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
<fx:root <fx:root type="StackPane" pickOnBounds="false" style="-fx-padding: 25;"
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
type="StackPane" pickOnBounds="false" style="-fx-padding: 25;"
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1"> xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<JFXButton prefWidth="150" prefHeight="50" buttonType="RAISED" styleClass="jfx-button-raised" <JFXButton prefWidth="150" prefHeight="50" buttonType="RAISED" styleClass="jfx-button-raised"
style="-fx-font-size: 15;" onMouseClicked="#launch" style="-fx-font-size: 15;" onMouseClicked="#launch"