From b11776971196c797d5b99a25a6bf3eade9d98a08 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 25 Feb 2020 09:36:18 +0800 Subject: [PATCH] alt: initial UI refactor --- .../org/jackhuang/hmcl/ui/Controllers.java | 141 ++------- .../jackhuang/hmcl/ui/LeftPaneController.java | 133 -------- .../account/AddAuthlibInjectorServerPane.java | 14 +- .../hmcl/ui/animation/AnimationProducer.java | 3 + .../ui/animation/ContainerAnimations.java | 54 ++++ ...sitionHandler.java => TransitionPane.java} | 33 +- .../hmcl/ui/construct/Navigator.java | 32 +- .../hmcl/ui/construct/SpinnerPane.java | 100 ++++-- .../ui/decorator/DecoratorController.java | 24 +- .../ui/decorator/DecoratorNavigatorPage.java | 87 ++++++ .../hmcl/ui/decorator/DecoratorPage.java | 12 +- .../hmcl/ui/decorator/DecoratorSkin.java | 64 ++-- .../ui/decorator/DecoratorTransitionPage.java | 93 ++++++ .../decorator/DecoratorWizardDisplayer.java | 55 ++-- .../hmcl/ui/download/VersionsPage.java | 15 +- .../hmcl/ui/{ => main}/MainPage.java | 11 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 287 ++++++++++++++++++ .../hmcl/ui/{ => main}/SettingsPage.java | 4 +- .../hmcl/ui/{ => main}/SettingsView.java | 4 +- .../hmcl/ui/versions/InstallerListPage.java | 17 +- .../hmcl/ui/versions/ModListPage.java | 27 +- .../hmcl/ui/versions/VersionPage.java | 29 +- .../hmcl/ui/versions/VersionRootPage.java | 140 +++++++++ .../jackhuang/hmcl/ui/versions/Versions.java | 4 +- .../hmcl/ui/versions/WorldListPage.java | 18 +- .../ui/wizard/DefaultWizardDisplayer.java | 9 +- .../jackhuang/hmcl/ui/wizard/Refreshable.java | 2 +- .../fxml/authlib-injector-server-add.fxml | 5 +- .../assets/fxml/download/versions.fxml | 5 +- .../assets/fxml/version/version.fxml | 2 - .../main/resources/assets/fxml/wizard.fxml | 3 +- 31 files changed, 948 insertions(+), 479 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/{TransitionHandler.java => TransitionPane.java} (74%) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/{ => main}/MainPage.java (96%) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/{ => main}/SettingsPage.java (99%) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/{ => main}/SettingsView.java (99%) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionRootPage.java 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 98cda500c..30e646154 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -23,58 +23,36 @@ import javafx.scene.layout.Region; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.EnumCommonDirectory; -import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.ui.account.AccountList; import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; -import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.decorator.DecoratorController; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.ui.profile.ProfileList; -import org.jackhuang.hmcl.ui.versions.GameItem; -import org.jackhuang.hmcl.ui.versions.GameList; -import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.ui.main.RootPage; +import org.jackhuang.hmcl.ui.versions.VersionRootPage; import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.platform.JavaVersion; -import org.jackhuang.hmcl.util.versioning.VersionNumber; -import java.io.File; -import java.util.Comparator; -import java.util.Date; -import java.util.List; import java.util.function.Consumer; -import java.util.stream.Collectors; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.newImage; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class Controllers { private static Scene scene; private static Stage stage; - private static MainPage mainPage = null; - private static SettingsPage settingsPage = null; - private static VersionPage versionPage = null; - private static GameList gameListPage = null; - private static AccountList accountListPage = null; - private static ProfileList profileListPage = null; + private static VersionRootPage versionPage = null; private static AuthlibInjectorServersPage serversPage = null; - private static LeftPaneController leftPaneController; + private static RootPage rootPage; private static DecoratorController decorator; public static Scene getScene() { @@ -86,53 +64,19 @@ public final class Controllers { } // FXThread - public static SettingsPage getSettingsPage() { - if (settingsPage == null) - settingsPage = new SettingsPage(); - return settingsPage; - } - - // FXThread - public static GameList getGameListPage() { - if (gameListPage == null) { - gameListPage = new GameList(); - FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { - File modpack = modpacks.get(0); - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); - }); - } - return gameListPage; - } - - // FXThread - public static AccountList getAccountListPage() { - if (accountListPage == null) { - AccountList accountListPage = new AccountList(); - accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty()); - accountListPage.accountsProperty().bindContent(Accounts.accountsProperty()); - Controllers.accountListPage = accountListPage; - } - return accountListPage; - } - - // FXThread - public static ProfileList getProfileListPage() { - if (profileListPage == null) { - ProfileList profileListPage = new ProfileList(); - profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); - profileListPage.profilesProperty().bindContent(Profiles.profilesProperty()); - Controllers.profileListPage = profileListPage; - } - return profileListPage; - } - - // FXThread - public static VersionPage getVersionPage() { + public static VersionRootPage getVersionPage() { if (versionPage == null) - versionPage = new VersionPage(); + versionPage = new VersionRootPage(); return versionPage; } + // FXThread + public static RootPage getRootPage() { + if (rootPage == null) + rootPage = new RootPage(); + return rootPage; + } + // FXThread public static AuthlibInjectorServersPage getServersPage() { if (serversPage == null) @@ -145,51 +89,6 @@ public final class Controllers { return decorator; } - public static MainPage getMainPage() { - if (mainPage == null) { - MainPage mainPage = new MainPage(); - FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { - File modpack = modpacks.get(0); - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); - }); - - FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> { - if (version != null) { - mainPage.setCurrentGame(version); - } else { - mainPage.setCurrentGame(i18n("version.empty")); - } - }); - mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty()); - mainPage.latestVersionProperty().bind( - BindingMapping.of(UpdateChecker.latestVersionProperty()) - .map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion()))); - - Profiles.registerVersionsListener(profile -> { - HMCLGameRepository repository = profile.getRepository(); - List children = repository.getVersions().parallelStream() - .filter(version -> !version.isHidden()) - .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) - .thenComparing(a -> VersionNumber.asVersion(a.getId()))) - .map(version -> { - Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId())); - node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId())); - return node; - }) - .collect(Collectors.toList()); - runInFX(() -> { - if (profile == Profiles.getSelectedProfile()) - mainPage.getVersions().setAll(children); - }); - }); - Controllers.mainPage = mainPage; - } - return mainPage; - } - - public static LeftPaneController getLeftPaneController() { - return leftPaneController; - } public static void initialize(Stage stage) { Logging.LOG.info("Start initializing application"); @@ -198,9 +97,7 @@ public final class Controllers { stage.setOnCloseRequest(e -> Launcher.stopApplication()); - decorator = new DecoratorController(stage, getMainPage()); - leftPaneController = new LeftPaneController(); - decorator.getDecorator().drawerProperty().setAll(leftPaneController); + decorator = new DecoratorController(stage, getRootPage()); if (config().getCommonDirType() == EnumCommonDirectory.CUSTOM && !FileUtils.canCreateDirectory(config().getCommonDirectory())) { @@ -261,7 +158,7 @@ public final class Controllers { } public static void navigate(Node node) { - decorator.getNavigator().navigate(node); + decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer()); } public static boolean isStopped() { @@ -269,15 +166,11 @@ public final class Controllers { } public static void shutdown() { - mainPage = null; - settingsPage = null; + rootPage = null; versionPage = null; serversPage = null; decorator = null; stage = null; scene = null; - gameListPage = null; - accountListPage = null; - profileListPage = null; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java deleted file mode 100644 index 958d8a104..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui; - -import javafx.application.Platform; -import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.RefreshedVersionsEvent; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.ModpackHelper; -import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.setting.Profiles; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem; -import org.jackhuang.hmcl.ui.account.AddAccountPane; -import org.jackhuang.hmcl.ui.construct.AdvancedListBox; -import org.jackhuang.hmcl.ui.construct.AdvancedListItem; -import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem; -import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; -import org.jackhuang.hmcl.ui.versions.Versions; -import org.jackhuang.hmcl.util.io.CompressingUtils; - -import java.io.File; - -import static org.jackhuang.hmcl.ui.FXUtils.newImage; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public final class LeftPaneController extends AdvancedListBox { - - public LeftPaneController() { - - AccountAdvancedListItem accountListItem = new AccountAdvancedListItem(); - accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage())); - accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); - - GameAdvancedListItem gameListItem = new GameAdvancedListItem(); - gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull()); - gameListItem.setOnAction(e -> { - Profile profile = Profiles.getSelectedProfile(); - String version = Profiles.getSelectedVersion(); - if (version == null) { - Controllers.navigate(Controllers.getGameListPage()); - } else { - Versions.modifyGameSettings(profile, version); - } - }); - - ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem(); - profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage())); - profileListItem.profileProperty().bind(Profiles.selectedProfileProperty()); - - AdvancedListItem gameItem = new AdvancedListItem(); - gameItem.setImage(newImage("/assets/img/bookshelf.png")); - gameItem.setTitle(i18n("version.manage")); - gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); - - AdvancedListItem launcherSettingsItem = new AdvancedListItem(); - launcherSettingsItem.setImage(newImage("/assets/img/command.png")); - launcherSettingsItem.setTitle(i18n("settings.launcher")); - launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage())); - - this - .startCategory(i18n("account").toUpperCase()) - .add(accountListItem) - .startCategory(i18n("version").toUpperCase()) - .add(gameListItem) - .add(gameItem) - .startCategory(i18n("profile.title").toUpperCase()) - .add(profileListItem) - .startCategory(i18n("launcher").toUpperCase()) - .add(launcherSettingsItem); - - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); - - Profile profile = Profiles.getSelectedProfile(); - if (profile != null && profile.getRepository().isLoaded()) - onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository()); - } - - // ==== Accounts ==== - public void checkAccount() { - if (Accounts.getAccounts().isEmpty()) - Platform.runLater(this::addNewAccount); - } - - private void addNewAccount() { - Controllers.dialog(new AddAccountPane()); - } - // ==== - - private boolean checkedModpack = false; - - private void onRefreshedVersions(HMCLGameRepository repository) { - runInFX(() -> { - if (!checkedModpack) { - checkedModpack = true; - - if (repository.getVersionCount() == 0) { - File modpackFile = new File("modpack.zip").getAbsoluteFile(); - if (modpackFile.exists()) { - Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath())) - .thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding)) - .thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack) - .withRunAsync(Schedulers.javafx(), this::checkAccount).executor()) - .thenAcceptAsync(Schedulers.javafx(), executor -> { - Controllers.taskDialog(executor, i18n("modpack.installing")); - executor.start(); - }).start(); - } - } - } - - checkAccount(); - }); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java index 9b4bacc49..b45540d9d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java @@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXTextField; - import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.StackPane; @@ -28,7 +27,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.SpinnerPane; @@ -44,7 +43,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware { - @FXML private StackPane addServerContainer; + @FXML private TransitionPane root; @FXML private Label lblServerUrl; @FXML private Label lblServerName; @FXML private Label lblCreationWarning; @@ -55,8 +54,6 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa @FXML private SpinnerPane nextPane; @FXML private JFXButton btnAddNext; - private TransitionHandler transitionHandler; - private AuthlibInjectorServer serverBeingAdded; public AddAuthlibInjectorServerPane(String url) { @@ -67,8 +64,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa public AddAuthlibInjectorServerPane() { loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml"); - transitionHandler = new TransitionHandler(addServerContainer); - transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer()); + root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer()); btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty()); nextPane.hideSpinner(); @@ -116,7 +112,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol())); - transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer()); + root.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer()); } else { LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception); lblCreationWarning.setText(resolveFetchExceptionMessage(exception)); @@ -127,7 +123,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa @FXML private void onAddPrev() { - transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer()); + root.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer()); } @FXML diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java index aaa17262b..10839c6bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.animation; import javafx.animation.KeyFrame; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -25,4 +26,6 @@ public interface AnimationProducer { void init(AnimationHandler handler); List animate(AnimationHandler handler); + + @Nullable AnimationProducer opposite(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java index e635ff6f5..5c423389a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java @@ -21,6 +21,8 @@ import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.util.Duration; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Collections; @@ -41,6 +43,7 @@ public enum ContainerAnimations { c.getCurrentNode().setScaleY(1); c.getCurrentNode().setOpacity(1); }, c -> Collections.emptyList()), + /** * A fade between the old and new view */ @@ -62,6 +65,36 @@ public enum ContainerAnimations { new KeyFrame(c.getDuration(), new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))), + + /** + * A fade between the old and new view + */ + FADE_IN(c -> { + c.getCurrentNode().setTranslateX(0); + c.getCurrentNode().setTranslateY(0); + c.getCurrentNode().setScaleX(1); + c.getCurrentNode().setScaleY(1); + c.getCurrentNode().setOpacity(0); + }, c -> + Arrays.asList(new KeyFrame(Duration.ZERO, + new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)), + new KeyFrame(c.getDuration(), + new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)))), + + /** + * A fade between the old and new view + */ + FADE_OUT(c -> { + c.getCurrentNode().setTranslateX(0); + c.getCurrentNode().setTranslateY(0); + c.getCurrentNode().setScaleX(1); + c.getCurrentNode().setScaleY(1); + c.getCurrentNode().setOpacity(1); + }, c -> + Arrays.asList(new KeyFrame(Duration.ZERO, + new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)), + new KeyFrame(c.getDuration(), + new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)))), /** * A zoom effect */ @@ -143,6 +176,7 @@ public enum ContainerAnimations { new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))); private final AnimationProducer animationProducer; + private ContainerAnimations opposite; ContainerAnimations(Consumer init, Function> animationProducer) { this.animationProducer = new AnimationProducer() { @@ -155,10 +189,30 @@ public enum ContainerAnimations { public List animate(AnimationHandler handler) { return animationProducer.apply(handler); } + + @Override + public @Nullable AnimationProducer opposite() { + return opposite != null ? opposite.getAnimationProducer() : null; + } }; } public AnimationProducer getAnimationProducer() { return animationProducer; } + + public ContainerAnimations getOpposite() { + return opposite; + } + + static { + NONE.opposite = NONE; + FADE.opposite = FADE; + SWIPE_LEFT.opposite = SWIPE_RIGHT; + SWIPE_RIGHT.opposite = SWIPE_LEFT; + FADE_IN.opposite = FADE_OUT; + FADE_OUT.opposite = FADE_IN; + ZOOM_IN.opposite = ZOOM_OUT; + ZOOM_OUT.opposite = ZOOM_IN; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java similarity index 74% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java index 20faa29a2..70add975e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java @@ -21,29 +21,18 @@ import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.layout.StackPane; -import javafx.scene.shape.Rectangle; import javafx.util.Duration; +import org.jackhuang.hmcl.ui.FXUtils; -public final class TransitionHandler implements AnimationHandler { - private final StackPane view; +public class TransitionPane extends StackPane implements AnimationHandler { private Timeline animation; private Duration duration; private Node previousNode, currentNode; - /** - * @param view A stack pane that contains another control that is {@link Parent} - */ - public TransitionHandler(StackPane view) { - this.view = view; - currentNode = view.getChildren().stream().findFirst().orElse(null); - - // prevent content overflow - Rectangle clip = new Rectangle(); - clip.widthProperty().bind(view.widthProperty()); - clip.heightProperty().bind(view.heightProperty()); - view.setClip(clip); + { + currentNode = getChildren().stream().findFirst().orElse(null); + FXUtils.setOverflowHidden(this, true); } @Override @@ -58,7 +47,7 @@ public final class TransitionHandler implements AnimationHandler { @Override public StackPane getCurrentRoot() { - return view; + return this; } @Override @@ -86,8 +75,8 @@ public final class TransitionHandler implements AnimationHandler { Timeline nowAnimation = new Timeline(); nowAnimation.getKeyFrames().addAll(transition.animate(this)); nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> { - view.setMouseTransparent(false); - view.getChildren().remove(previousNode); + setMouseTransparent(false); + getChildren().remove(previousNode); })); nowAnimation.play(); animation = nowAnimation; @@ -95,7 +84,7 @@ public final class TransitionHandler implements AnimationHandler { } private void updateContent(Node newView) { - if (view.getWidth() > 0 && view.getHeight() > 0) { + if (getWidth() > 0 && getHeight() > 0) { previousNode = currentNode; if (previousNode == null) previousNode = EMPTY_PANE; @@ -105,11 +94,11 @@ public final class TransitionHandler implements AnimationHandler { if (previousNode == newView) previousNode = EMPTY_PANE; - view.setMouseTransparent(true); + setMouseTransparent(true); currentNode = newView; - view.getChildren().setAll(previousNode, currentNode); + getChildren().setAll(previousNode, currentNode); } private final StackPane EMPTY_PANE = new StackPane(); 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 index 7d22bb643..910a65a0a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java @@ -24,21 +24,20 @@ import javafx.event.EventHandler; 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.AnimationProducer; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.util.Logging; import java.util.Optional; import java.util.Stack; import java.util.logging.Level; -public class Navigator extends StackPane { +public class Navigator extends TransitionPane { 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 boolean initialized = false; public void init(Node init) { @@ -50,7 +49,7 @@ public class Navigator extends StackPane { initialized = true; } - public void navigate(Node node) { + public void navigate(Node node, AnimationProducer animationProducer) { FXUtils.checkFxUserThread(); if (!initialized) @@ -68,7 +67,8 @@ public class Navigator extends StackPane { fireEvent(navigating); node.fireEvent(navigating); - setContent(node); + node.getProperties().put("hmcl.navigator.animation", animationProducer); + setContent(node, animationProducer); NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED); fireEvent(navigated); @@ -110,7 +110,12 @@ public class Navigator extends StackPane { fireEvent(navigating); node.fireEvent(navigating); - setContent(node); + Object obj = from.getProperties().get("hmcl.navigator.animation"); + if (obj instanceof AnimationProducer) { + setContent(node, (AnimationProducer) obj); + } else { + setContent(node, ContainerAnimations.NONE.getAnimationProducer()); + } NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED); fireEvent(navigated); @@ -128,8 +133,8 @@ public class Navigator extends StackPane { return stack.size() > 1; } - private void setContent(Node content) { - animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer()); + public void setContent(Node content, AnimationProducer animationProducer) { + super.setContent(content, animationProducer); if (content instanceof Region) { ((Region) content).setMinSize(0, 0); @@ -179,14 +184,21 @@ public class Navigator extends StackPane { public static final EventType NAVIGATED = new EventType<>("NAVIGATED"); public static final EventType NAVIGATING = new EventType<>("NAVIGATING"); + private final Navigator source; private final Node node; - public NavigationEvent(Object source, Node target, EventType eventType) { + public NavigationEvent(Navigator source, Node target, EventType eventType) { super(source, target, eventType); + this.source = source; this.node = target; } + @Override + public Navigator getSource() { + return source; + } + public Node getNode() { return node; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java index c107665a9..55499d145 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java @@ -18,38 +18,28 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.controls.JFXSpinner; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.beans.DefaultProperty; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.animation.AnimationHandler; +import org.jackhuang.hmcl.ui.animation.AnimationProducer; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionHandler; @DefaultProperty("content") -public class SpinnerPane extends StackPane { - private final TransitionHandler transitionHandler = new TransitionHandler(this); - private final JFXSpinner spinner = new JFXSpinner(); - private final StackPane contentPane = new StackPane(); +public class SpinnerPane extends Control { private final ObjectProperty content = new SimpleObjectProperty<>(this, "content"); - private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading") { - protected void invalidated() { - if (get()) - transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer()); - else - transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer()); - } - }; - - public SpinnerPane() { - getStyleClass().add("spinner-pane"); - - getChildren().setAll(contentPane); - - content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue)); - } + private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading"); public void showSpinner() { setLoading(true); @@ -82,4 +72,72 @@ public class SpinnerPane extends StackPane { public void setLoading(boolean loading) { this.loading.set(loading); } + + @Override + protected Skin createDefaultSkin() { + return new Skin(this); + } + + private static class Skin extends SkinBase { + private final JFXSpinner spinner = new JFXSpinner(); + private final StackPane contentPane = new StackPane(); + private final StackPane topPane = new StackPane(); + private final StackPane root = new StackPane(); + private Timeline animation; + + protected Skin(SpinnerPane control) { + super(control); + + root.getStyleClass().add("spinner-pane"); + topPane.getChildren().setAll(spinner); + root.getChildren().setAll(contentPane, topPane); + FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue)); + getChildren().setAll(root); + + FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> { + Timeline prev = animation; + if (prev != null) prev.stop(); + + AnimationProducer transition; + topPane.setMouseTransparent(true); + topPane.setVisible(true); + topPane.getStyleClass().add("gray-background"); + if (newValue) + transition = ContainerAnimations.FADE_IN.getAnimationProducer(); + else + transition = ContainerAnimations.FADE_OUT.getAnimationProducer(); + + AnimationHandler handler = new AnimationHandler() { + @Override + public Duration getDuration() { + return Duration.millis(160); + } + + @Override + public Pane getCurrentRoot() { + return root; + } + + @Override + public Node getPreviousNode() { + return null; + } + + @Override + public Node getCurrentNode() { + return topPane; + } + }; + + Timeline now = new Timeline(); + now.getKeyFrames().addAll(transition.animate(handler)); + now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> { + topPane.setMouseTransparent(!newValue); + topPane.setVisible(newValue); + })); + now.play(); + animation = now; + }); + } + } } 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 index d0667562e..ced703cb1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.Navigator; @@ -220,8 +221,8 @@ public class DecoratorController { if (navigator.getCurrentPage() instanceof DecoratorPage) { DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); - if (page.canForceToClose()) { - page.onForceToClose(); + if (page.isPageCloseable()) { + page.closePage(); return; } } @@ -232,7 +233,7 @@ public class DecoratorController { if (navigator.getCurrentPage() instanceof DecoratorPage) { DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); - if (page.onClose()) + if (page.back()) navigator.close(); } else { navigator.close(); @@ -243,23 +244,25 @@ public class DecoratorController { if (navigator.getCurrentPage() instanceof Refreshable) { Refreshable refreshable = (Refreshable) navigator.getCurrentPage(); - if (refreshable.canRefreshProperty().get()) + if (refreshable.refreshableProperty().get()) refreshable.refresh(); } } private void onNavigating(Navigator.NavigationEvent event) { + if (event.getSource() != this.navigator) return; Node from = event.getNode(); if (from instanceof DecoratorPage) - ((DecoratorPage) from).onClose(); + ((DecoratorPage) from).back(); } private void onNavigated(Navigator.NavigationEvent event) { + if (event.getSource() != this.navigator) return; Node to = event.getNode(); if (to instanceof Refreshable) { - decorator.canRefreshProperty().bind(((Refreshable) to).canRefreshProperty()); + decorator.canRefreshProperty().bind(((Refreshable) to).refreshableProperty()); } else { decorator.canRefreshProperty().unbind(); decorator.canRefreshProperty().set(false); @@ -267,14 +270,17 @@ public class DecoratorController { if (to instanceof DecoratorPage) { decorator.drawerTitleProperty().bind(((DecoratorPage) to).titleProperty()); - decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).canForceToClose()); + decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable()); + decorator.canBackProperty().bind(Bindings.createBooleanBinding(() -> navigator.canGoBack() || ((DecoratorPage) to).backableProperty().get(), + ((DecoratorPage) to).backableProperty())); } else { decorator.drawerTitleProperty().unbind(); decorator.drawerTitleProperty().set(""); decorator.showCloseAsHomeProperty().set(true); + decorator.canBackProperty().unbind(); + decorator.canBackProperty().setValue(navigator.canGoBack()); } - decorator.canBackProperty().set(navigator.canGoBack()); decorator.canCloseProperty().set(navigator.canGoBack()); if (to instanceof Region) { @@ -359,7 +365,7 @@ public class DecoratorController { public void startWizard(WizardProvider wizardProvider, String category) { FXUtils.checkFxUserThread(); - getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category)); + getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer()); } // ==== Authlib Injector DnD ==== diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java new file mode 100644 index 000000000..6c67b02e2 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java @@ -0,0 +1,87 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.decorator; + +import javafx.scene.Node; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.animation.AnimationProducer; +import org.jackhuang.hmcl.ui.construct.Navigator; +import org.jackhuang.hmcl.ui.wizard.Refreshable; + +public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage { + protected final Navigator navigator = new Navigator(); + + { + this.navigator.setOnNavigating(this::onNavigating); + this.navigator.setOnNavigated(this::onNavigated); + } + + @Override + protected void navigate(Node page, AnimationProducer animationProducer) { + navigator.navigate(page, animationProducer); + setRefreshable(page instanceof Refreshable); + } + + @Override + public boolean back() { + if (navigator.canGoBack()) { + navigator.close(); + return false; + } else { + return true; + } + } + + private void onNavigating(Navigator.NavigationEvent event) { + if (event.getSource() != this.navigator) return; + Node from = event.getNode(); + + if (from instanceof DecoratorPage) + ((DecoratorPage) from).back(); + } + + private void onNavigated(Navigator.NavigationEvent event) { + if (event.getSource() != this.navigator) return; + Node to = event.getNode(); + + if (to instanceof Refreshable) { + refreshableProperty().bind(((Refreshable) to).refreshableProperty()); + } else { + refreshableProperty().unbind(); + refreshableProperty().set(false); + } + + if (to instanceof DecoratorPage) { + titleProperty().bind(((DecoratorPage) to).titleProperty()); + } else { + titleProperty().unbind(); + titleProperty().set(""); + } + + setBackable(navigator.canGoBack()); + + 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()); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java index 86532a959..eba652ee1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java @@ -17,22 +17,28 @@ */ package org.jackhuang.hmcl.ui.decorator; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.Node; import org.jackhuang.hmcl.ui.construct.Navigator; public interface DecoratorPage { ReadOnlyStringProperty titleProperty(); - default boolean canForceToClose() { + default boolean isPageCloseable() { return false; } - default boolean onClose() { + default boolean back() { return true; } - default void onForceToClose() { + default BooleanProperty backableProperty() { + return new SimpleBooleanProperty(true); + } + + default void closePage() { } default void onDecoratorPageNavigating(Navigator.NavigationEvent event) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java index 16d9354ff..1b65fd9c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -72,65 +72,41 @@ public class DecoratorSkin extends SkinBase { root.setMaxWidth(Region.USE_PREF_SIZE); root.setMinWidth(Region.USE_PREF_SIZE); - StackPane drawerWrapper = new StackPane(); - skinnable.setDrawerWrapper(drawerWrapper); - drawerWrapper.getStyleClass().add("jfx-decorator-drawer"); - drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty()); - FXUtils.setOverflowHidden(drawerWrapper, true); + // center node with a container node in bottom layer and a "welcome" layer at the top layer. + StackPane container = new StackPane(); + skinnable.setDrawerWrapper(container); + container.getStyleClass().add("jfx-decorator-drawer"); + container.backgroundProperty().bind(skinnable.backgroundProperty()); + FXUtils.setOverflowHidden(container, true); + // bottom layer { - BorderPane drawer = new BorderPane(); + contentPlaceHolder = new StackPane(); + contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container"); + Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty()); - { - BorderPane leftRootPane = new BorderPane(); - FXUtils.setLimitWidth(leftRootPane, 200); - leftRootPane.getStyleClass().add("jfx-decorator-content-container"); - - StackPane drawerContainer = new StackPane(); - drawerContainer.getStyleClass().add("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().add("jfx-decorator-content-container"); - FXUtils.setOverflowHidden(contentPlaceHolder, true); - Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty()); - - drawer.setCenter(contentPlaceHolder); - } - - drawerWrapper.getChildren().add(drawer); + container.getChildren().add(contentPlaceHolder); } + // top layer for welcome and hint { - StackPane container = new StackPane(); - Bindings.bindContent(container.getChildren(), skinnable.containerProperty()); + StackPane floatLayer = new StackPane(); + Bindings.bindContent(floatLayer.getChildren(), skinnable.containerProperty()); ListChangeListener listener = c -> { if (skinnable.getContainer().isEmpty()) { - container.setMouseTransparent(true); - container.setVisible(false); + floatLayer.setMouseTransparent(true); + floatLayer.setVisible(false); } else { - container.setMouseTransparent(false); - container.setVisible(true); + floatLayer.setMouseTransparent(false); + floatLayer.setVisible(true); } }; skinnable.containerProperty().addListener(listener); listener.onChanged(null); - drawerWrapper.getChildren().add(container); + container.getChildren().add(floatLayer); } - root.setCenter(drawerWrapper); + root.setCenter(container); titleContainer = new BorderPane(); titleContainer.setOnMouseReleased(this::onMouseReleased); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java new file mode 100644 index 000000000..73522d7f4 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java @@ -0,0 +1,93 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +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.control.Control; +import javafx.scene.control.Skin; +import org.jackhuang.hmcl.ui.animation.AnimationProducer; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.wizard.Refreshable; + +public abstract class DecoratorTransitionPage extends Control implements DecoratorPage, Refreshable { + private final StringProperty title = new SimpleStringProperty(); + private final BooleanProperty refreshable = new SimpleBooleanProperty(false); + private final BooleanProperty backable = new SimpleBooleanProperty(); + private Node currentPage; + protected final TransitionPane transitionPane = new TransitionPane(); + + protected void navigate(Node page, AnimationProducer animation) { + transitionPane.setContent(currentPage = page, animation); + refreshable.setValue(page instanceof Refreshable); + } + + @Override + protected abstract Skin createDefaultSkin(); + + protected Node getCurrentPage() { + return currentPage; + } + + public String getTitle() { + return title.get(); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } + + public boolean isRefreshable() { + return refreshable.get(); + } + + @Override + public void refresh() { + // empty implementation for default + } + + @Override + public BooleanProperty refreshableProperty() { + return refreshable; + } + + public void setRefreshable(boolean refreshable) { + this.refreshable.set(refreshable); + } + + public boolean isBackable() { + return backable.get(); + } + + @Override + public BooleanProperty backableProperty() { + return backable; + } + + public void setBackable(boolean backable) { + this.backable.set(backable); + } +} 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 index 3b7080c8f..fdfef24e3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java @@ -17,13 +17,8 @@ */ 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 javafx.scene.control.SkinBase; import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.wizard.*; @@ -31,18 +26,12 @@ 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); +public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements TaskExecutorDialogWizardDisplayer { 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); } @@ -56,16 +45,6 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating); } - @Override - public StringProperty titleProperty() { - return title; - } - - @Override - public BooleanProperty canRefreshProperty() { - return canRefresh; - } - @Override public WizardController getWizardController() { return wizardController; @@ -88,30 +67,26 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD @Override public void navigateTo(Node page, Navigation.NavigationDirection nav) { - nowPage = page; - - transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer()); - - canRefresh.set(page instanceof Refreshable); + navigate(page, nav.getAnimation().getAnimationProducer()); String prefix = category == null ? "" : category + " - "; if (page instanceof WizardPage) - title.set(prefix + ((WizardPage) page).getTitle()); + setTitle(prefix + ((WizardPage) page).getTitle()); } @Override - public boolean canForceToClose() { + public boolean isPageCloseable() { return true; } @Override - public void onForceToClose() { + public void closePage() { wizardController.onCancel(); } @Override - public boolean onClose() { + public boolean back() { if (wizardController.canPrev()) { wizardController.onPrev(true); return false; @@ -121,6 +96,20 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD @Override public void refresh() { - ((Refreshable) nowPage).refresh(); + ((Refreshable) getCurrentPage()).refresh(); + } + + @Override + protected Skin createDefaultSkin() { + return new Skin(this); + } + + private static class Skin extends SkinBase { + + protected Skin(DecoratorWizardDisplayer control) { + super(control); + + getChildren().setAll(control.transitionPane); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 253b37dad..605ac6ed4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -42,7 +42,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.FloatListCell; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.wizard.Refreshable; @@ -72,7 +72,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres @FXML private StackPane emptyPane; @FXML - private StackPane root; + private TransitionPane root; @FXML private JFXCheckBox chkRelease; @FXML @@ -84,7 +84,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres @FXML private VBox centrePane; - private final TransitionHandler transitionHandler; private final VersionList versionList; private TaskExecutor executor; @@ -97,8 +96,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml"); - transitionHandler = new TransitionHandler(root); - if (versionList.hasType()) { centrePane.getChildren().setAll(checkPane, list); } else @@ -187,14 +184,14 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres @Override public void refresh() { - transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer()); + root.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer()); executor = versionList.refreshAsync(gameVersion).whenComplete(exception -> { if (exception == null) { List items = loadVersions(); Platform.runLater(() -> { if (versionList.getVersions(gameVersion).isEmpty()) { - transitionHandler.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer()); + root.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer()); } else { if (items.isEmpty()) { chkRelease.setSelected(true); @@ -203,13 +200,13 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres } else { list.getItems().setAll(items); } - transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer()); + root.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer()); } }); } else { LOG.log(Level.WARNING, "Failed to fetch versions list", exception); Platform.runLater(() -> { - transitionHandler.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer()); + root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer()); }); } }).executor().start(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java similarity index 96% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 1fbea92fd..2abdd0174 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui; +package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXPopup; @@ -23,12 +23,7 @@ import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; @@ -44,6 +39,8 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java new file mode 100644 index 000000000..4ea831e04 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -0,0 +1,287 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.main; + +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.ModpackHelper; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem; +import org.jackhuang.hmcl.ui.account.AccountList; +import org.jackhuang.hmcl.ui.account.AddAccountPane; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.ui.construct.PopupMenu; +import org.jackhuang.hmcl.ui.decorator.DecoratorNavigatorPage; +import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; +import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem; +import org.jackhuang.hmcl.ui.profile.ProfileList; +import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; +import org.jackhuang.hmcl.ui.versions.GameItem; +import org.jackhuang.hmcl.ui.versions.GameList; +import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jackhuang.hmcl.util.versioning.VersionNumber; + +import java.io.File; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class RootPage extends DecoratorNavigatorPage { + private MainPage mainPage = null; + private SettingsPage settingsPage = null; + private GameList gameListPage = null; + private AccountList accountListPage = null; + private ProfileList profileListPage = null; + + public RootPage() { + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); + + Profile profile = Profiles.getSelectedProfile(); + if (profile != null && profile.getRepository().isLoaded()) + onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository()); + } + + @Override + protected Skin createDefaultSkin() { + return new Skin(this); + } + + private MainPage getMainPage() { + if (mainPage == null) { + mainPage = new MainPage(); + FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { + File modpack = modpacks.get(0); + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); + }); + + FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> { + if (version != null) { + mainPage.setCurrentGame(version); + } else { + mainPage.setCurrentGame(i18n("version.empty")); + } + }); + mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty()); + mainPage.latestVersionProperty().bind( + BindingMapping.of(UpdateChecker.latestVersionProperty()) + .map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion()))); + + Profiles.registerVersionsListener(profile -> { + HMCLGameRepository repository = profile.getRepository(); + List children = repository.getVersions().parallelStream() + .filter(version -> !version.isHidden()) + .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) + .thenComparing(a -> VersionNumber.asVersion(a.getId()))) + .map(version -> { + Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId())); + node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId())); + return node; + }) + .collect(Collectors.toList()); + runInFX(() -> { + if (profile == Profiles.getSelectedProfile()) + mainPage.getVersions().setAll(children); + }); + }); + } + return mainPage; + } + + private SettingsPage getSettingsPage() { + if (settingsPage == null) + settingsPage = new SettingsPage(); + return settingsPage; + } + + private GameList getGameListPage() { + if (gameListPage == null) { + gameListPage = new GameList(); + FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { + File modpack = modpacks.get(0); + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); + }); + } + return gameListPage; + } + + private AccountList getAccountListPage() { + if (accountListPage == null) { + accountListPage = new AccountList(); + accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty()); + accountListPage.accountsProperty().bindContent(Accounts.accountsProperty()); + } + return accountListPage; + } + + private ProfileList getProfileListPage() { + if (profileListPage == null) { + profileListPage = new ProfileList(); + profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); + profileListPage.profilesProperty().bindContent(Profiles.profilesProperty()); + } + return profileListPage; + } + + private static class Skin extends SkinBase { + + protected Skin(RootPage control) { + super(control); + + // first item in left sidebar + AccountAdvancedListItem accountListItem = new AccountAdvancedListItem(); + accountListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getAccountListPage(), ContainerAnimations.FADE.getAnimationProducer())); + accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); + + // second item in left sidebar + GameAdvancedListItem gameListItem = new GameAdvancedListItem(); + gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull()); + gameListItem.setOnAction(e -> { + Profile profile = Profiles.getSelectedProfile(); + String version = Profiles.getSelectedVersion(); + if (version == null) { + getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer()); + } else { + Versions.modifyGameSettings(profile, version); + } + }); + + // third item in left sidebar + AdvancedListItem gameItem = new AdvancedListItem(); + gameItem.setImage(newImage("/assets/img/bookshelf.png")); + gameItem.setTitle(i18n("version.manage")); + gameItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer())); + + // forth item in left sidebar + ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem(); + profileListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getProfileListPage(), ContainerAnimations.FADE.getAnimationProducer())); + profileListItem.profileProperty().bind(Profiles.selectedProfileProperty()); + + // fifth item in left sidebar + AdvancedListItem launcherSettingsItem = new AdvancedListItem(); + launcherSettingsItem.setImage(newImage("/assets/img/command.png")); + launcherSettingsItem.setTitle(i18n("settings.launcher")); + launcherSettingsItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getSettingsPage(), ContainerAnimations.FADE.getAnimationProducer())); + + // the left sidebar + AdvancedListBox sideBar = new AdvancedListBox() + .startCategory(i18n("account").toUpperCase()) + .add(accountListItem) + .startCategory(i18n("version").toUpperCase()) + .add(gameListItem) + .add(gameItem) + .startCategory(i18n("profile.title").toUpperCase()) + .add(profileListItem) + .startCategory(i18n("launcher").toUpperCase()) + .add(launcherSettingsItem); + + // the root page, with the sidebar in left, navigator in center. + BorderPane root = new BorderPane(); + + { + BorderPane leftRootPane = new BorderPane(); + FXUtils.setLimitWidth(leftRootPane, 200); + + StackPane drawerContainer = new StackPane(); + drawerContainer.getStyleClass().add("gray-background"); + drawerContainer.getChildren().setAll(sideBar); + leftRootPane.setCenter(drawerContainer); + + Rectangle separator = new Rectangle(); + separator.heightProperty().bind(root.heightProperty()); + separator.setWidth(1); + separator.setFill(Color.GRAY); + + leftRootPane.setRight(separator); + + root.setLeft(leftRootPane); + } + + { + control.navigator.getStyleClass().add("jfx-decorator-content-container"); + control.navigator.init(getSkinnable().getMainPage()); + root.setCenter(control.navigator); + } + + getChildren().setAll(root); + } + + } + + // ==== Accounts ==== + public void checkAccount() { + if (Accounts.getAccounts().isEmpty()) + Platform.runLater(this::addNewAccount); + } + + private void addNewAccount() { + Controllers.dialog(new AddAccountPane()); + } + // ==== + + private boolean checkedModpack = false; + + private void onRefreshedVersions(HMCLGameRepository repository) { + runInFX(() -> { + if (!checkedModpack) { + checkedModpack = true; + + if (repository.getVersionCount() == 0) { + File modpackFile = new File("modpack.zip").getAbsoluteFile(); + if (modpackFile.exists()) { + Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath())) + .thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding)) + .thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack) + .withRunAsync(Schedulers.javafx(), this::checkAccount).executor()) + .thenAcceptAsync(Schedulers.javafx(), executor -> { + Controllers.taskDialog(executor, i18n("modpack.installing")); + executor.start(); + }).start(); + } + } + } + + checkAccount(); + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 92fdbb97f..c2b09dfa9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui; +package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXColorPicker; import com.jfoenix.effects.JFXDepthManager; @@ -31,6 +31,8 @@ import javafx.scene.paint.Color; import javafx.scene.text.Font; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.*; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.Validator; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java index a719a0d45..9a5788b44 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui; +package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.*; import javafx.geometry.HPos; @@ -31,6 +31,8 @@ import javafx.scene.text.TextAlignment; import org.jackhuang.hmcl.setting.EnumBackgroundImage; import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java index 34b0d9ae0..450241d11 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.versions; +import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.Skin; import javafx.stage.FileChooser; @@ -28,12 +29,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskListener; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.InstallerItem; -import org.jackhuang.hmcl.ui.ListPageBase; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.ToolbarListPageSkin; +import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.download.InstallerWizardProvider; import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider; import org.jackhuang.hmcl.util.Lang; @@ -43,6 +39,7 @@ import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; @@ -67,17 +64,17 @@ public class InstallerListPage extends ListPageBase { return new InstallerListPageSkin(); } - public void loadVersion(Profile profile, String versionId) { + public CompletableFuture loadVersion(Profile profile, String versionId) { this.profile = profile; this.versionId = versionId; this.version = profile.getRepository().getVersion(versionId); this.gameVersion = null; - Task.supplyAsync(() -> { + return CompletableFuture.supplyAsync(() -> { gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse(null); return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId)); - }).thenAcceptAsync(Schedulers.javafx(), analyzer -> { + }).thenAcceptAsync(analyzer -> { Function> removeAction = libraryId -> x -> { profile.getDependency().removeLibraryAsync(version, libraryId) .thenComposeAsync(profile.getRepository()::save) @@ -100,7 +97,7 @@ public class InstallerListPage extends ListPageBase { else itemsProperty().add(new InstallerItem(title, libraryVersion, null, action)); } - }).start(); + }, Platform::runLater); } public void installOnline() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 14fc899df..62f9a1f90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXTabPane; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ObservableList; @@ -37,10 +38,12 @@ import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.stream.Collectors; @@ -77,28 +80,32 @@ public final class ModListPage extends ListPageBase loadVersion(Profile profile, String id) { libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id)); modded.set(libraryAnalyzer.hasModLoader()); - loadMods(profile.getRepository().getModManager(id)); + return loadMods(profile.getRepository().getModManager(id)); } - private void loadMods(ModManager modManager) { + private CompletableFuture loadMods(ModManager modManager) { this.modManager = modManager; - Task.supplyAsync(() -> { - synchronized (ModListPage.this) { - runInFX(() -> loadingProperty().set(true)); - modManager.refreshMods(); - return new LinkedList<>(modManager.getMods()); + return CompletableFuture.supplyAsync(() -> { + try { + synchronized (ModListPage.this) { + runInFX(() -> loadingProperty().set(true)); + modManager.refreshMods(); + return new LinkedList<>(modManager.getMods()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); } - }).whenComplete(Schedulers.javafx(), (list, exception) -> { + }).whenCompleteAsync((list, exception) -> { loadingProperty().set(false); if (exception == null) FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> { if (newValue != null && newValue.getUserData() == ModListPage.this) itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList())); }); - }).start(); + }, Platform::runLater); } public void add() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 3995bcb10..7fe49bdd8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXPopup; import com.jfoenix.controls.JFXTabPane; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.fxml.FXML; @@ -34,15 +36,16 @@ import org.jackhuang.hmcl.ui.construct.IconedMenuItem; import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.construct.PopupMenu; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; +import java.util.concurrent.CompletableFuture; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class VersionPage extends StackPane implements DecoratorPage { +public final class VersionPage extends StackPane { private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", null); + private final ReadOnlyBooleanWrapper loading = new ReadOnlyBooleanWrapper(this, "loading", false); @FXML private VersionSettingsPage versionSettings; @@ -65,8 +68,6 @@ public final class VersionPage extends StackPane implements DecoratorPage { @FXML private JFXButton btnTestGame; @FXML - private StackPane rootPane; - @FXML private StackPane contentPane; @FXML private JFXTabPane tabPane; @@ -107,7 +108,6 @@ public final class VersionPage extends StackPane implements DecoratorPage { btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20)); FXUtils.installFastTooltip(btnTestGame, i18n("version.launch.test")); - addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); } @@ -120,9 +120,13 @@ public final class VersionPage extends StackPane implements DecoratorPage { versionSettings.loadVersion(profile, id); mod.setParentTab(tabPane); modTab.setUserData(mod); - mod.loadVersion(profile, id); - installer.loadVersion(profile, id); - world.loadVersion(profile, id); + loading.set(true); + + CompletableFuture.allOf( + mod.loadVersion(profile, id), + installer.loadVersion(profile, id), + world.loadVersion(profile, id)) + .whenCompleteAsync((result, exception) -> loading.set(false), Platform::runLater); } private void onNavigated(Navigator.NavigationEvent event) { @@ -163,7 +167,6 @@ public final class VersionPage extends StackPane implements DecoratorPage { return title.get(); } - @Override public ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } @@ -171,4 +174,12 @@ public final class VersionPage extends StackPane implements DecoratorPage { public void setTitle(String title) { this.title.set(title); } + + public boolean isLoading() { + return loading.get(); + } + + public ReadOnlyBooleanProperty loadingProperty() { + return loading.getReadOnlyProperty(); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionRootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionRootPage.java new file mode 100644 index 000000000..a0ecd1aeb --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionRootPage.java @@ -0,0 +1,140 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXListView; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.Control; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.versioning.VersionNumber; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; + +public class VersionRootPage extends Control implements DecoratorPage { + private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(); + private final BooleanProperty loading = new SimpleBooleanProperty(); + private final JFXListView listView = new JFXListView<>(); + private final VersionPage versionPage = new VersionPage(); + private Profile profile; + + { + Profiles.registerVersionsListener(this::loadVersions); + + listView.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> { + loadVersion(newValue, profile); + }); + } + + private void loadVersions(Profile profile) { + HMCLGameRepository repository = profile.getRepository(); + List children = repository.getVersions().parallelStream() + .filter(version -> !version.isHidden()) + .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) + .thenComparing(a -> VersionNumber.asVersion(a.getId()))) + .map(Version::getId) + .collect(Collectors.toList()); + runInFX(() -> { + if (profile == Profiles.getSelectedProfile()) { + this.profile = profile; + loading.set(false); + listView.getItems().setAll(children); + } + }); + } + + public void loadVersion(String version, Profile profile) { + listView.getSelectionModel().select(version); + versionPage.load(version, profile); + } + + @Override + protected Skin createDefaultSkin() { + return new Skin(this); + } + + @Override + public ReadOnlyStringProperty titleProperty() { + return title.getReadOnlyProperty(); + } + + public static class Skin extends SkinBase { + + /** + * Constructor for all SkinBase instances. + * + * @param control The control for which this Skin should attach to. + */ + protected Skin(VersionRootPage control) { + super(control); + + control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + SpinnerPane spinnerPane = new SpinnerPane(); + spinnerPane.getStyleClass().add("large-spinner-pane"); + + // the root page, with the sidebar in left, navigator in center. + BorderPane root = new BorderPane(); + root.getStyleClass().add("gray-background"); + + { + BorderPane leftRootPane = new BorderPane(); + FXUtils.setLimitWidth(leftRootPane, 200); + + StackPane drawerContainer = new StackPane(); + drawerContainer.getChildren().setAll(control.listView); + leftRootPane.setCenter(drawerContainer); + + Rectangle separator = new Rectangle(); + separator.heightProperty().bind(root.heightProperty()); + separator.setWidth(1); + separator.setFill(Color.GRAY); + + leftRootPane.setRight(separator); + + root.setLeft(leftRootPane); + } + + root.setCenter(control.versionPage); + control.title.bind(control.versionPage.titleProperty()); + + spinnerPane.loadingProperty().bind(control.versionPage.loadingProperty()); + spinnerPane.setContent(root); + getChildren().setAll(spinnerPane); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index 4e3e1f283..e1ac7c6dc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -121,7 +121,7 @@ public class Versions { private static boolean checkForLaunching(Profile profile, String id) { if (Accounts.getSelectedAccount() == null) - Controllers.getLeftPaneController().checkAccount(); + Controllers.getRootPage().checkAccount(); else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) Controllers.dialog(i18n("version.empty.launch")); else @@ -136,7 +136,7 @@ public class Versions { } public static void modifyGameSettings(Profile profile, String version) { - Controllers.getVersionPage().load(version, profile); + Controllers.getVersionPage().loadVersion(version, profile); Controllers.navigate(Controllers.getVersionPage()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index 1cdbbb861..b73c0445a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXCheckBox; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.Node; @@ -39,6 +40,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.stream.Collectors; @@ -71,29 +73,29 @@ public class WorldListPage extends ListPageBase { return new WorldListPageSkin(); } - public void loadVersion(Profile profile, String id) { + public CompletableFuture loadVersion(Profile profile, String id) { this.profile = profile; this.id = id; this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves"); - refresh(); + return refresh(); } - public void refresh() { + public CompletableFuture refresh() { if (profile == null || id == null) - return; + return CompletableFuture.completedFuture(null); setLoading(true); - Task + return CompletableFuture .runAsync(() -> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(null)) - .thenSupplyAsync(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList())) - .whenComplete(Schedulers.javafx(), (result, exception) -> { + .thenApplyAsync(unused -> World.getWorlds(savesDir).parallel().collect(Collectors.toList())) + .whenCompleteAsync((result, exception) -> { worlds = result; setLoading(false); if (exception == null) itemsProperty().setAll(result.stream() .filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)) .map(WorldListItem::new).collect(Collectors.toList())); - }).start(); + }, Platform::runLater); } public void add() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.java index fe2ec5efa..f24e67876 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.java @@ -24,7 +24,7 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.util.StringUtils; import java.util.Queue; @@ -38,10 +38,8 @@ public class DefaultWizardDisplayer extends StackPane implements AbstractWizardD private Node nowPage; - private TransitionHandler transitionHandler; - @FXML - private StackPane root; + private TransitionPane root; @FXML private JFXButton backButton; @FXML @@ -86,7 +84,7 @@ public class DefaultWizardDisplayer extends StackPane implements AbstractWizardD @Override public void navigateTo(Node page, Navigation.NavigationDirection nav) { backButton.setDisable(!wizardController.canPrev()); - transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer()); + root.setContent(page, nav.getAnimation().getAnimationProducer()); String title = StringUtils.isBlank(prefix) ? "" : prefix + " - "; if (page instanceof WizardPage) titleLabel.setText(title + ((WizardPage) page).getTitle()); @@ -96,7 +94,6 @@ public class DefaultWizardDisplayer extends StackPane implements AbstractWizardD @FXML private void initialize() { - transitionHandler = new TransitionHandler(root); wizardController.onStart(); } 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 db7cde606..952995c35 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 @@ -23,7 +23,7 @@ import javafx.beans.property.SimpleBooleanProperty; public interface Refreshable { void refresh(); - default BooleanProperty canRefreshProperty() { + default BooleanProperty refreshableProperty() { return new SimpleBooleanProperty(false); } } diff --git a/HMCL/src/main/resources/assets/fxml/authlib-injector-server-add.fxml b/HMCL/src/main/resources/assets/fxml/authlib-injector-server-add.fxml index 85ab474a7..aa0d11590 100644 --- a/HMCL/src/main/resources/assets/fxml/authlib-injector-server-add.fxml +++ b/HMCL/src/main/resources/assets/fxml/authlib-injector-server-add.fxml @@ -5,10 +5,11 @@ + - + - + diff --git a/HMCL/src/main/resources/assets/fxml/download/versions.fxml b/HMCL/src/main/resources/assets/fxml/download/versions.fxml index 68296fb9d..454142834 100644 --- a/HMCL/src/main/resources/assets/fxml/download/versions.fxml +++ b/HMCL/src/main/resources/assets/fxml/download/versions.fxml @@ -3,6 +3,7 @@ +
- + @@ -30,6 +31,6 @@ - +
diff --git a/HMCL/src/main/resources/assets/fxml/version/version.fxml b/HMCL/src/main/resources/assets/fxml/version/version.fxml index 7099359a7..af5e5f455 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version.fxml @@ -10,9 +10,7 @@ - diff --git a/HMCL/src/main/resources/assets/fxml/wizard.fxml b/HMCL/src/main/resources/assets/fxml/wizard.fxml index 24994f178..a7f3dfd34 100644 --- a/HMCL/src/main/resources/assets/fxml/wizard.fxml +++ b/HMCL/src/main/resources/assets/fxml/wizard.fxml @@ -5,6 +5,7 @@ + - +