From 5fb0035b2f2cbe40bee7badb407c37a0d4d7bda4 Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Sun, 2 Sep 2018 21:53:34 +0800 Subject: [PATCH] Refactor decorator as well as navigation --- .../jackhuang/hmcl/ui/AdvancedListItem2.java | 12 +- .../org/jackhuang/hmcl/ui/Controllers.java | 24 +- .../java/org/jackhuang/hmcl/ui/Decorator.java | 733 ------------------ .../jackhuang/hmcl/ui/LeftPaneController.java | 9 +- .../java/org/jackhuang/hmcl/ui/MainPage.java | 5 +- .../org/jackhuang/hmcl/ui/SettingsPage.java | 2 +- .../org/jackhuang/hmcl/ui/VersionPage.java | 2 +- .../hmcl/ui/account/AccountList.java | 2 +- .../account/AuthlibInjectorServersPage.java | 2 +- .../hmcl/ui/construct/DialogCloseEvent.java | 2 +- .../hmcl/ui/construct/Navigator.java | 155 ++++ .../hmcl/ui/construct/PageCloseEvent.java | 41 + .../hmcl/ui/decorator/DecoratorControl.java | 201 +++++ .../ui/decorator/DecoratorController.java | 374 +++++++++ .../{wizard => decorator}/DecoratorPage.java | 12 +- .../hmcl/ui/decorator/DecoratorSkin.java | 422 ++++++++++ .../decorator/DecoratorWizardDisplayer.java | 126 +++ .../ui/download/DownloadWizardProvider.java | 10 +- .../hmcl/ui/profile/ProfileList.java | 2 +- .../hmcl/ui/profile/ProfilePage.java | 3 +- .../jackhuang/hmcl/ui/versions/GameList.java | 9 +- .../hmcl/ui/versions/GameListItem.java | 2 +- .../jackhuang/hmcl/ui/wizard/Refreshable.java | 7 + .../TaskExecutorDialogWizardDisplayer.java | 0 HMCL/src/main/resources/assets/css/root.css | 4 + .../main/resources/assets/fxml/decorator.fxml | 2 +- HMCL/src/main/resources/assets/fxml/main.fxml | 4 +- 27 files changed, 1381 insertions(+), 786 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/{wizard => decorator}/DecoratorPage.java (80%) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/{construct => wizard}/TaskExecutorDialogWizardDisplayer.java (100%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListItem2.java index 449a5fd76..8c660746f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListItem2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListItem2.java @@ -60,21 +60,11 @@ public class AdvancedListItem2 extends Control { return onActionProperty().get(); } - private ObjectProperty> onAction = new ObjectPropertyBase>() { + private ObjectProperty> onAction = new SimpleObjectProperty>(this, "onAction") { @Override protected void invalidated() { setEventHandler(ActionEvent.ACTION, get()); } - - @Override - public Object getBean() { - return AdvancedListItem2.this; - } - - @Override - public String getName() { - return "onAction"; - } }; @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index a4a7314d4..69c3f39a7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -32,6 +32,7 @@ import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.MessageBox; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; 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.versions.GameList; import org.jackhuang.hmcl.util.FutureCallback; @@ -53,7 +54,7 @@ public final class Controllers { private static ProfileList profileListPage = null; private static AuthlibInjectorServersPage serversPage = null; private static LeftPaneController leftPaneController; - private static Decorator decorator; + private static DecoratorController decorator; public static Scene getScene() { return scene; @@ -106,7 +107,7 @@ public final class Controllers { } // FXThread - public static Decorator getDecorator() { + public static DecoratorController getDecorator() { return decorator; } @@ -125,20 +126,14 @@ public final class Controllers { stage.setOnCloseRequest(e -> Launcher.stopApplication()); - decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true); - decorator.showPage(null); - leftPaneController = new LeftPaneController(decorator.getLeftPane()); + decorator = new DecoratorController(stage, getMainPage()); + leftPaneController = new LeftPaneController(); + decorator.getDecorator().drawerProperty().setAll(leftPaneController); Task.of(JavaVersion::initialize).start(); - decorator.setCustomMaximize(false); - - scene = new Scene(decorator, 804, 521); + scene = new Scene(decorator.getDecorator(), 800, 519); 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.setTitle(Metadata.TITLE); @@ -189,10 +184,7 @@ public final class Controllers { } public static void navigate(Node node) { - if (decorator.getNowPage() == node) - decorator.showPage(null); - else - decorator.showPage(node); + decorator.getNavigator().navigate(node); } public static boolean isStopped() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java deleted file mode 100644 index 91ac7e21b..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java +++ /dev/null @@ -1,733 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * 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 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 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 = 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 randomImageIn(Path imageDir) { - if (!Files.isDirectory(imageDir)) { - return Optional.empty(); - } - - List 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 loaded = tryLoadImage(candidates.get(selected)); - if (loaded.isPresent()) { - return Optional.of(loaded.get()); - } else { - candidates.remove(selected); - } - } - return Optional.empty(); - } - - private Optional 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 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) 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 getCancelQueue() { - return cancelQueue; - } - - public Runnable getOnCloseButtonAction() { - return onCloseButtonAction.get(); - } - - public ObjectProperty 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)))); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index cb2eb266c..07c3acfae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -43,9 +43,10 @@ import java.util.concurrent.atomic.AtomicReference; 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(); accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage())); accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); @@ -67,10 +68,10 @@ public final class LeftPaneController { .then(Color.RED) .otherwise(Color.BLACK)); - launcherSettingsItem.maxWidthProperty().bind(leftPane.widthProperty()); + launcherSettingsItem.maxWidthProperty().bind(widthProperty()); launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); - leftPane + this .startCategory(i18n("account").toUpperCase()) .add(accountListItem) .startCategory(i18n("version").toUpperCase()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java index 22e0ac691..af12d5e23 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java @@ -23,18 +23,17 @@ import javafx.fxml.FXML; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.setting.Profile; 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.wizard.DecoratorPage; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class MainPage extends StackPane implements DecoratorPage { private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page")); - { FXUtils.loadFXML(this, "/assets/fxml/main.fxml"); - } @FXML diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java index 8b35d9eb7..b3588e87c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -36,7 +36,7 @@ import javafx.scene.paint.Color; import javafx.scene.text.Font; import org.jackhuang.hmcl.setting.*; 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.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChecker; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java index 47a938fd9..35e924311 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java @@ -28,8 +28,8 @@ import javafx.scene.control.Tab; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; 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.wizard.DecoratorPage; import org.jackhuang.hmcl.util.FileUtils; import java.io.File; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java index e2293af2c..9164e908c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java @@ -25,7 +25,7 @@ import javafx.scene.control.ToggleGroup; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.setting.Accounts; 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 static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java index 29d5e33bd..70c20d4c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java @@ -23,7 +23,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; 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 javafx.beans.binding.Bindings; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java index 6b7d7656b..b4282dacd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java @@ -32,7 +32,7 @@ import javafx.scene.layout.Region; */ public class DialogCloseEvent extends Event { - public static final EventType CLOSE = new EventType<>("CLOSE"); + public static final EventType CLOSE = new EventType<>("DIALOG_CLOSE"); public DialogCloseEvent() { super(CLOSE); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java new file mode 100644 index 000000000..15e50c090 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java @@ -0,0 +1,155 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 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 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) 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 getOnNavigated() { + return onNavigated.get(); + } + + public ObjectProperty> onNavigatedProperty() { + return onNavigated; + } + + public void setOnNavigated(EventHandler onNavigated) { + this.onNavigated.set(onNavigated); + } + + private ObjectProperty> onNavigated = new SimpleObjectProperty>(this, "onNavigated") { + @Override + protected void invalidated() { + setEventHandler(NavigationEvent.NAVIGATED, get()); + } + }; + + public EventHandler getOnNavigating() { + return onNavigating.get(); + } + + public ObjectProperty> onNavigatingProperty() { + return onNavigating; + } + + public void setOnNavigating(EventHandler onNavigating) { + this.onNavigating.set(onNavigating); + } + + private ObjectProperty> onNavigating = new SimpleObjectProperty>(this, "onNavigating") { + @Override + protected void invalidated() { + setEventHandler(NavigationEvent.NAVIGATING, get()); + } + }; + + public static class NavigationEvent extends Event { + public static final EventType NAVIGATED = new EventType<>("NAVIGATED"); + public static final EventType NAVIGATING = new EventType<>("NAVIGATING"); + + private final Node node; + + public NavigationEvent(Object source, Node target, EventType eventType) { + super(source, target, eventType); + + this.node = target; + } + + public Node getNode() { + return node; + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java new file mode 100644 index 000000000..55601ded6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java @@ -0,0 +1,41 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 CLOSE = new EventType<>("PAGE_CLOSE"); + + public PageCloseEvent() { + super(CLOSE); + } + + public PageCloseEvent(Object source, EventTarget target) { + super(source, target, CLOSE); + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java new file mode 100644 index 000000000..6725fe552 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java @@ -0,0 +1,201 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 drawer = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty content = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty container = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ObjectProperty contentBackground = new SimpleObjectProperty<>(); + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty drawerTitle = new SimpleStringProperty(); + private final ObjectProperty onCloseButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> onCloseNavButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> onBackNavButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> 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 getDrawer() { + return drawer.get(); + } + + public ListProperty drawerProperty() { + return drawer; + } + + public void setDrawer(ObservableList drawer) { + this.drawer.set(drawer); + } + + public ObservableList getContent() { + return content.get(); + } + + public ListProperty contentProperty() { + return content; + } + + public void setContent(ObservableList 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 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 getContainer() { + return container.get(); + } + + public ListProperty containerProperty() { + return container; + } + + public void setContainer(ObservableList container) { + this.container.set(container); + } + + public Background getContentBackground() { + return contentBackground.get(); + } + + public ObjectProperty 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> onBackNavButtonActionProperty() { + return onBackNavButtonAction; + } + + public ObjectProperty> onCloseNavButtonActionProperty() { + return onCloseNavButtonAction; + } + + public ObjectProperty> onRefreshNavButtonActionProperty() { + return onRefreshNavButtonAction; + } + + @Override + protected Skin createDefaultSkin() { + return new DecoratorSkin(this); + } + + public void minimize() { + primaryStage.setIconified(true); + } + + public void close() { + onCloseButtonAction.get().run(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java new file mode 100644 index 000000000..469b4b4ba --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -0,0 +1,374 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 = 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 randomImageIn(Path imageDir) { + if (!Files.isDirectory(imageDir)) { + return Optional.empty(); + } + + List 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 loaded = tryLoadImage(candidates.get(selected)); + if (loaded.isPresent()) { + return loaded; + } else { + candidates.remove(selected); + } + } + return Optional.empty(); + } + + private Optional 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 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) 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 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 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)))); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java similarity index 80% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java index 239e3ee5e..658c73e3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java @@ -15,13 +15,21 @@ * 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.wizard; +package org.jackhuang.hmcl.ui.decorator; import javafx.beans.property.StringProperty; public interface DecoratorPage { StringProperty titleProperty(); - default void onClose() { + default boolean canForceToClose() { + return false; + } + + default boolean onClose() { + return true; + } + + default void onForceToClose() { } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java new file mode 100644 index 000000000..f43925753 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -0,0 +1,422 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 { + 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 listener = new ListChangeListener() { + @Override + public void onChanged(Change 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(); + } + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java new file mode 100644 index 000000000..7e16b0c62 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java @@ -0,0 +1,126 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 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 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(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java index b1121f6b7..e11925936 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; @@ -53,7 +54,8 @@ public final class DownloadWizardProvider implements WizardProvider { private Task finishVersionDownloadingAsync(Map settings) { 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()); if (settings.containsKey("forge")) @@ -65,7 +67,8 @@ public final class DownloadWizardProvider implements WizardProvider { if (settings.containsKey("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 settings) { @@ -77,7 +80,8 @@ public final class DownloadWizardProvider implements WizardProvider { String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(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 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java index 70ca881bb..fec55ec7b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java @@ -25,7 +25,7 @@ import javafx.scene.control.ToggleGroup; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; 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 static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java index cd7961387..b85c904ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java @@ -27,11 +27,10 @@ import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; -import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; 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 static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java index f048e493a..ec7bfc4d3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java @@ -31,7 +31,7 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.Controllers; 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.i18n.I18n; @@ -78,6 +78,13 @@ public class GameList extends Control implements DecoratorPage { loading.set(false); items.setAll(children); 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) -> { GameListItem model = (GameListItem) toggle.getUserData(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index e8f5f1c5b..daf47bcee 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -127,7 +127,7 @@ public class GameListItem extends Control { public void modifyGameSettings() { Controllers.getVersionPage().load(version, profile); - Controllers.getDecorator().showPage(Controllers.getVersionPage()); + Controllers.navigate(Controllers.getVersionPage()); } public void generateLaunchScript() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java index 0c8fd8320..e4a70aa0b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java @@ -17,6 +17,13 @@ */ package org.jackhuang.hmcl.ui.wizard; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + public interface Refreshable { void refresh(); + + default BooleanProperty canRefreshProperty() { + return new SimpleBooleanProperty(false); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java similarity index 100% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogWizardDisplayer.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 57e7fde41..486a6deaa 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1062,6 +1062,10 @@ .jfx-decorator-drawer { } +.jfx-decorator-title { + -fx-text-fill: -fx-base-text-fill; -fx-font-size: 15; +} + .resize-border { -fx-border-color: -fx-base-color; -fx-border-width: 0 2 2 2; diff --git a/HMCL/src/main/resources/assets/fxml/decorator.fxml b/HMCL/src/main/resources/assets/fxml/decorator.fxml index fa26b7b10..4c2d9a52b 100644 --- a/HMCL/src/main/resources/assets/fxml/decorator.fxml +++ b/HMCL/src/main/resources/assets/fxml/decorator.fxml @@ -22,7 +22,7 @@ maxWidth="800">
- + diff --git a/HMCL/src/main/resources/assets/fxml/main.fxml b/HMCL/src/main/resources/assets/fxml/main.fxml index 3345f3fbc..11cf40672 100644 --- a/HMCL/src/main/resources/assets/fxml/main.fxml +++ b/HMCL/src/main/resources/assets/fxml/main.fxml @@ -2,9 +2,7 @@ -