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..cbce9dda5 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.main.RootPage; import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.upgrade.UpdateChecker; 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 AuthlibInjectorServersPage serversPage = null; - private static LeftPaneController leftPaneController; + private static RootPage rootPage; private static DecoratorController decorator; public static Scene getScene() { @@ -85,47 +63,6 @@ public final class Controllers { return stage; } - // 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() { if (versionPage == null) @@ -133,6 +70,13 @@ public final class Controllers { 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/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 87e694b35..b5f6f1d19 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -45,6 +45,7 @@ import javafx.scene.shape.Rectangle; import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.ResourceNotFoundError; import org.jackhuang.hmcl.util.i18n.I18n; @@ -100,8 +101,10 @@ public final class FXUtils { value.addListener((a, b, c) -> consumer.accept(c)); } - public static void onWeakChange(ObservableValue value, Consumer consumer) { - value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c))); + public static WeakChangeListener onWeakChange(ObservableValue value, Consumer consumer) { + WeakChangeListener listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c)); + value.addListener(listener); + return listener; } public static void onChangeAndOperate(ObservableValue value, Consumer consumer) { @@ -109,9 +112,9 @@ public final class FXUtils { onChange(value, consumer); } - public static void onWeakChangeAndOperate(ObservableValue value, Consumer consumer) { + public static WeakChangeListener onWeakChangeAndOperate(ObservableValue value, Consumer consumer) { consumer.accept(value.getValue()); - onWeakChange(value, consumer); + return onWeakChange(value, consumer); } public static void runLaterIf(BooleanSupplier condition, Runnable runnable) { @@ -201,19 +204,19 @@ public final class FXUtils { return field.getProperties().containsKey("FXUtils.validation"); } - public static void setOverflowHidden(Region region, boolean hidden) { - if (hidden) { - Rectangle rectangle = new Rectangle(); - rectangle.widthProperty().bind(region.widthProperty()); - rectangle.heightProperty().bind(region.heightProperty()); - region.setClip(rectangle); - } else { - region.setClip(null); - } + public static Rectangle setOverflowHidden(Region region) { + Rectangle rectangle = new Rectangle(); + rectangle.widthProperty().bind(region.widthProperty()); + rectangle.heightProperty().bind(region.heightProperty()); + region.setClip(rectangle); + return rectangle; } - public static boolean getOverflowHidden(Region region) { - return region.getClip() != null; + public static Rectangle setOverflowHidden(Region region, double arc) { + Rectangle rectangle = setOverflowHidden(region); + rectangle.setArcWidth(arc); + rectangle.setArcHeight(arc); + return rectangle; } public static void setLimitWidth(Region region, double width) { @@ -452,7 +455,7 @@ public final class FXUtils { derivatives[i] += derivatives[i - 1]; double dy = derivatives[derivatives.length - 1]; double height = listView.getLayoutBounds().getHeight(); - bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1)); + bar.setValue(Lang.clamp(0, bar.getValue() + dy / height, 1)); if (Math.abs(dy) < 0.001) timeline.stop(); listView.requestLayout(); 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/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index cfd9e679e..790f4dcbc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -178,4 +178,8 @@ public final class SVG { public static Node arrowRight(ObjectBinding fill, double width, double height) { return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height); } + + public static Node wrench(ObjectBinding fill, double width, double height) { + return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java index cc466e14e..ee905d20b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java @@ -44,12 +44,13 @@ public abstract class ToolbarListPageSkin BorderPane root = new BorderPane(); - { + List toolbarButtons = initializeToolbar(skinnable); + if (!toolbarButtons.isEmpty()) { HBox toolbar = new HBox(); toolbar.getStyleClass().add("jfx-tool-bar-second"); JFXDepthManager.setDepth(toolbar, 1); toolbar.setPickOnBounds(false); - toolbar.getChildren().setAll(initializeToolbar(skinnable)); + toolbar.getChildren().setAll(toolbarButtons); root.setTop(toolbar); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java index e5093866f..ff1b4650e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java @@ -15,23 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2019 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 com.jfoenix.controls.JFXButton; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java index c43bf83c0..a6491f4dc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -70,9 +70,9 @@ public class AccountAdvancedListItem extends AdvancedListItem { ObservableList accounts = Accounts.getAccounts(); int currentIndex = accounts.indexOf(account.get()); if (event.getDeltaY() > 0) { // up - currentIndex += 1; + currentIndex--; } else { // down - currentIndex -= 1; + currentIndex++; } Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size())); }); 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 1f61514ba..e3848c3c5 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 @@ -24,11 +24,12 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.ListPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.javafx.MappedObservableList; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; public class AccountList extends ListPage implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage")); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage"))); private final ListProperty accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList()); private final ObjectProperty selectedAccount; @@ -51,7 +52,7 @@ public class AccountList extends ListPage implements DecoratorP } @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } } 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/account/AuthlibInjectorServersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java index a20044b41..d10aa329e 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 @@ -18,8 +18,7 @@ package org.jackhuang.hmcl.ui.account; import javafx.beans.binding.Bindings; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ObservableList; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.ui.Controllers; @@ -31,7 +30,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class AuthlibInjectorServersPage extends ListPage implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title")); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.injector.manage.title"))); private final ObservableList serverItems; @@ -50,16 +49,8 @@ public class AuthlibInjectorServersPage extends ListPage stateProperty() { + return state; } } 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..9a5f15339 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); } @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/ComponentListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentListCell.java index 059919a93..234eefb38 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentListCell.java @@ -126,7 +126,7 @@ class ComponentListCell extends StackPane { VBox container = new VBox(); container.setPadding(new Insets(8, 0, 0, 0)); FXUtils.setLimitHeight(container, 0); - FXUtils.setOverflowHidden(container, true); + FXUtils.setOverflowHidden(container); container.getChildren().setAll(content); groupNode.setBottom(container); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java new file mode 100644 index 000000000..4188fe623 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java @@ -0,0 +1,195 @@ +/** + * 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.construct; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.NumberBinding; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.Skin; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import org.jackhuang.hmcl.util.Lang; + +public class FloatScrollBarSkin implements Skin { + private ScrollBar scrollBar; + private Region group; + private Rectangle track = new Rectangle(); + private Rectangle thumb = new Rectangle(); + + public FloatScrollBarSkin(final ScrollBar scrollBar) { + this.scrollBar = scrollBar; + scrollBar.setPrefHeight(1e-18); + scrollBar.setPrefWidth(1e-18); + + this.group = new Region() { + Point2D dragStart; + double preDragThumbPos; + + NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty()); + NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range); + + { + // Children are added unmanaged because for some reason the height of the bar keeps changing + // if they're managed in certain situations... not sure about the cause. + getChildren().addAll(track, thumb); + + track.setManaged(false); + track.getStyleClass().add("track"); + + thumb.setManaged(false); + thumb.getStyleClass().add("thumb"); + + scrollBar.orientationProperty().addListener(obs -> setup()); + + setup(); + + + thumb.setOnMousePressed(me -> { + if (me.isSynthesized()) { + // touch-screen events handled by Scroll handler + me.consume(); + return; + } + /* + ** if max isn't greater than min then there is nothing to do here + */ + if (getSkinnable().getMax() > getSkinnable().getMin()) { + dragStart = thumb.localToParent(me.getX(), me.getY()); + double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); + preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); + me.consume(); + } + }); + + + thumb.setOnMouseDragged(me -> { + if (me.isSynthesized()) { + // touch-screen events handled by Scroll handler + me.consume(); + return; + } + /* + ** if max isn't greater than min then there is nothing to do here + */ + if (getSkinnable().getMax() > getSkinnable().getMin()) { + /* + ** if the tracklength isn't greater then do nothing.... + */ + if (trackLength() > thumbLength()) { + Point2D cur = thumb.localToParent(me.getX(), me.getY()); + if (dragStart == null) { + // we're getting dragged without getting a mouse press + dragStart = thumb.localToParent(me.getX(), me.getY()); + } + double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); + double position = preDragThumbPos + dragPos / (trackLength() - thumbLength()); + if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus(); + double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin(); + if (!Double.isNaN(newValue)) { + getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax())); + } + } + + me.consume(); + } + }); + } + + private double trackLength() { + return getSkinnable().getOrientation() == Orientation.VERTICAL ? track.getHeight() : track.getWidth(); + } + + private double thumbLength() { + return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth(); + } + + private double boundedSize(double min, double value, double max) { + return Math.min(Math.max(value, min), Math.max(min, max)); + } + + private void setup() { + track.widthProperty().unbind(); + track.heightProperty().unbind(); + + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + track.relocate(0, -5); + track.widthProperty().bind(scrollBar.widthProperty()); + track.setHeight(5); + } else { + track.relocate(-5, 0); + track.setWidth(5); + track.heightProperty().bind(scrollBar.heightProperty()); + } + + thumb.xProperty().unbind(); + thumb.yProperty().unbind(); + thumb.widthProperty().unbind(); + thumb.heightProperty().unbind(); + + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + thumb.relocate(0, -5); + thumb.widthProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty()))); + thumb.setHeight(5); + thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position)); + } else { + thumb.relocate(-5, 0); + thumb.setWidth(5); + thumb.heightProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty()))); + thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position)); + } + } + + @Override + protected double computeMaxWidth(double height) { + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + return Double.MAX_VALUE; + } + + return 5; + } + + @Override + protected double computeMaxHeight(double width) { + if (scrollBar.getOrientation() == Orientation.VERTICAL) { + return Double.MAX_VALUE; + } + + return 5; + } + }; + } + + @Override + public void dispose() { + scrollBar = null; + group = null; + } + + @Override + public Node getNode() { + return group; + } + + @Override + public ScrollBar getSkinnable() { + return scrollBar; + } +} 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..f7dd09d3a 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,10 +67,10 @@ 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); node.fireEvent(navigated); EventHandler handler = event -> close(node); @@ -110,10 +109,14 @@ 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); node.fireEvent(navigated); Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) @@ -128,12 +131,16 @@ public class Navigator extends StackPane { return stack.size() > 1; } - private void setContent(Node content) { - animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer()); + public int size() { + return stack.size(); + } + + public void setContent(Node content, AnimationProducer animationProducer) { + super.setContent(content, animationProducer); if (content instanceof Region) { ((Region) content).setMinSize(0, 0); - FXUtils.setOverflowHidden((Region) content, true); + FXUtils.setOverflowHidden((Region) content); } } @@ -179,14 +186,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/construct/TabHeader.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java new file mode 100644 index 000000000..60298af9e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java @@ -0,0 +1,504 @@ +/* + * 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.construct; + +import com.jfoenix.controls.JFXRippler; +import javafx.animation.*; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Side; +import javafx.scene.AccessibleAttribute; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Scale; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.javafx.MappedObservableList; + +public class TabHeader extends Control { + + public TabHeader(Tab... tabs) { + getStyleClass().setAll("tab-header"); + if (tabs != null) { + getTabs().addAll(tabs); + } + } + + private ObservableList tabs = FXCollections.observableArrayList(); + + public ObservableList getTabs() { + return tabs; + } + + private final ObjectProperty> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabHeaderSelectionModel(this)); + + public SingleSelectionModel getSelectionModel() { + return selectionModel.get(); + } + + public ObjectProperty> selectionModelProperty() { + return selectionModel; + } + + public void setSelectionModel(SingleSelectionModel selectionModel) { + this.selectionModel.set(selectionModel); + } + + static class TabHeaderSelectionModel extends SingleSelectionModel { + private final TabHeader tabHeader; + + public TabHeaderSelectionModel(final TabHeader t) { + if (t == null) { + throw new NullPointerException("TabPane can not be null"); + } + this.tabHeader = t; + + // watching for changes to the items list content + final ListChangeListener itemsContentObserver = c -> { + while (c.next()) { + for (Tab tab : c.getRemoved()) { + if (tab != null && !tabHeader.getTabs().contains(tab)) { + if (tab.isSelected()) { + tab.setSelected(false); + final int tabIndex = c.getFrom(); + + // we always try to select the nearest, non-disabled + // tab from the position of the closed tab. + findNearestAvailableTab(tabIndex, true); + } + } + } + if (c.wasAdded() || c.wasRemoved()) { + // The selected tab index can be out of sync with the list of tab if + // we add or remove tabs before the selected tab. + if (getSelectedIndex() != tabHeader.getTabs().indexOf(getSelectedItem())) { + clearAndSelect(tabHeader.getTabs().indexOf(getSelectedItem())); + } + } + } + if (getSelectedIndex() == -1 && getSelectedItem() == null && tabHeader.getTabs().size() > 0) { + // we go looking for the first non-disabled tab, as opposed to + // just selecting the first tab (fix for RT-36908) + findNearestAvailableTab(0, true); + } else if (tabHeader.getTabs().isEmpty()) { + clearSelection(); + } + }; + if (this.tabHeader.getTabs() != null) { + this.tabHeader.getTabs().addListener(itemsContentObserver); + } + } + + // API Implementation + @Override public void select(int index) { + if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) || + (index == getSelectedIndex() && getModelItem(index).isSelected())) { + return; + } + + // Unselect the old tab + if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) { + tabHeader.getTabs().get(getSelectedIndex()).setSelected(false); + } + + setSelectedIndex(index); + + Tab tab = getModelItem(index); + if (tab != null) { + setSelectedItem(tab); + } + + // Select the new tab + if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) { + tabHeader.getTabs().get(getSelectedIndex()).setSelected(true); + } + + /* Does this get all the change events */ + tabHeader.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); + } + + @Override public void select(Tab tab) { + final int itemCount = getItemCount(); + + for (int i = 0; i < itemCount; i++) { + final Tab value = getModelItem(i); + if (value != null && value.equals(tab)) { + select(i); + return; + } + } + if (tab != null) { + setSelectedItem(tab); + } + } + + @Override protected Tab getModelItem(int index) { + final ObservableList items = tabHeader.getTabs(); + if (items == null) return null; + if (index < 0 || index >= items.size()) return null; + return items.get(index); + } + + @Override protected int getItemCount() { + final ObservableList items = tabHeader.getTabs(); + return items == null ? 0 : items.size(); + } + + private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) { + // we always try to select the nearest, non-disabled + // tab from the position of the closed tab. + final int tabCount = getItemCount(); + int i = 1; + Tab bestTab = null; + while (true) { + // look leftwards + int downPos = tabIndex - i; + if (downPos >= 0) { + Tab _tab = getModelItem(downPos); + if (_tab != null) { + bestTab = _tab; + break; + } + } + + // look rightwards. We subtract one as we need + // to take into account that a tab has been removed + // and if we don't do this we'll miss the tab + // to the right of the tab (as it has moved into + // the removed tabs position). + int upPos = tabIndex + i - 1; + if (upPos < tabCount) { + Tab _tab = getModelItem(upPos); + if (_tab != null) { + bestTab = _tab; + break; + } + } + + if (downPos < 0 && upPos >= tabCount) { + break; + } + i++; + } + + if (doSelect && bestTab != null) { + select(bestTab); + } + + return bestTab; + } + } + + @Override + protected Skin createDefaultSkin() { + return new TabHeaderSkin(this); + } + + public static class TabHeaderSkin extends SkinBase { + + private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected"); + + private final Color ripplerColor = Color.valueOf("#FFFF8D"); + + private final HeaderContainer header; + private boolean isSelectingTab = false; + private Tab selectedTab; + + protected TabHeaderSkin(TabHeader control) { + super(control); + + header = new HeaderContainer(); + getChildren().setAll(header); + + FXUtils.onChangeAndOperate(control.getSelectionModel().selectedItemProperty(), item -> { + isSelectingTab = true; + selectedTab = item; + Platform.runLater(() -> { + header.setNeedsLayout2(true); + header.layout(); + }); + }); + + this.selectedTab = control.getSelectionModel().getSelectedItem(); + if (this.selectedTab == null && control.getSelectionModel().getSelectedIndex() != -1) { + control.getSelectionModel().select(control.getSelectionModel().getSelectedIndex()); + this.selectedTab = control.getSelectionModel().getSelectedItem(); + } + + if (this.selectedTab == null) { + control.getSelectionModel().selectFirst(); + } + + this.selectedTab = control.getSelectionModel().getSelectedItem(); + } + + protected class HeaderContainer extends StackPane { + private Timeline timeline; + private StackPane selectedTabLine; + private StackPane headersRegion; + private Scale scale = new Scale(1, 1, 0, 0); + private Rotate rotate = new Rotate(0, 0, 1); + private double selectedTabLineOffset; + private ObservableList binding; + + public HeaderContainer() { + getStyleClass().add("tab-header-area"); + setPickOnBounds(false); + + headersRegion = new StackPane() { + @Override + protected double computePrefWidth(double height) { + double width = 0; + for (Node child : getChildren()) { + if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; + width += child.prefWidth(height); + } + return snapSize(width) + snappedLeftInset() + snappedRightInset(); + } + + @Override + protected double computePrefHeight(double width) { + double height = 0; + for (Node child : getChildren()) { + if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; + height = Math.max(height, child.prefHeight(width)); + } + return snapSize(height) + snappedTopInset() + snappedBottomInset(); + } + + @Override + protected void layoutChildren() { + if (isSelectingTab) { + animateSelectionLine(); + isSelectingTab = false; + } + + double headerHeight = snapSize(prefHeight(-1)); + double tabStartX = 0; + for (Node node : getChildren()) { + if (!(node instanceof TabHeaderContainer)) continue; + TabHeaderContainer child = (TabHeaderContainer) node; + double w = snapSize(child.prefWidth(-1)); + double h = snapSize(child.prefHeight(-1)); + child.resize(w, h); + + child.relocate(tabStartX, headerHeight - h - snappedBottomInset()); + tabStartX += w; + } + + selectedTabLine.resizeRelocate(0, + headerHeight - selectedTabLine.prefHeight(-1), + snapSize(selectedTabLine.prefWidth(-1)), + snapSize(selectedTabLine.prefHeight(-1))); + } + }; + + selectedTabLine = new StackPane(); + selectedTabLine.setManaged(false); + selectedTabLine.getTransforms().addAll(scale, rotate); + selectedTabLine.setCache(true); + selectedTabLine.getStyleClass().addAll("tab-selected-line"); + selectedTabLine.setPrefHeight(2); + selectedTabLine.setPrefWidth(1); + selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY))); + getChildren().setAll(headersRegion, selectedTabLine); + headersRegion.setPickOnBounds(false); + headersRegion.prefHeightProperty().bind(heightProperty()); + prefWidthProperty().bind(headersRegion.widthProperty()); + + Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> { + TabHeaderContainer container = new TabHeaderContainer(tab); + container.setVisible(true); + return container; + })); + } + + public void setNeedsLayout2(boolean value) { + setNeedsLayout(value); + } + + private void runTimeline(double newTransX, double newWidth) { + double tempScaleX = 0.0D; + double tempWidth = 0.0D; + double lineWidth = this.selectedTabLine.prefWidth(-1.0D); + if (this.isAnimating()) { + this.timeline.stop(); + tempScaleX = this.scale.getX(); + if (this.rotate.getAngle() != 0.0D) { + this.rotate.setAngle(0.0D); + tempWidth = tempScaleX * lineWidth; + this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth); + } + } + + double oldScaleX = this.scale.getX(); + double oldWidth = lineWidth * oldScaleX; + double oldTransX = this.selectedTabLine.getTranslateX(); + double newScaleX = newWidth * oldScaleX / oldWidth; + this.selectedTabLineOffset = newTransX; + // newTransX += offsetStart * (double)this.direction; + double transDiff = newTransX - oldTransX; + double midScaleX = tempScaleX != 0.0D ? tempScaleX : (Math.abs(transDiff) / 1.3D + oldWidth) * oldScaleX / oldWidth; + if (transDiff < 0.0D) { + this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth); + newTransX += newWidth; + this.rotate.setAngle(180.0D); + } + + this.timeline = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.12D), new KeyValue(this.scale.xProperty(), midScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.24D), new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH))); + this.timeline.setOnFinished((finish) -> { + if (this.rotate.getAngle() != 0.0D) { + this.rotate.setAngle(0.0D); + this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth); + } + + }); + this.timeline.play(); + } + + private boolean isAnimating() { + return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING; + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + if (isSelectingTab) { + animateSelectionLine(); + isSelectingTab = false; + } + } + + private void animateSelectionLine() { + double offset = 0.0D; + double selectedTabOffset = 0.0D; + double selectedTabWidth = 0.0D; + Side side = Side.TOP; + + for (Node node : headersRegion.getChildren()) { + if (node instanceof TabHeaderContainer) { + TabHeaderContainer tabHeader = (TabHeaderContainer)node; + double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D)); + if (selectedTab != null && selectedTab.equals(tabHeader.tab)) { + selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth; + selectedTabWidth = tabHeaderPrefWidth; + break; + } + + offset += tabHeaderPrefWidth; + } + } + + this.runTimeline(selectedTabOffset, selectedTabWidth); + } + } + + protected class TabHeaderContainer extends StackPane { + + private final Tab tab; + private final Label tabText; + private final BorderPane inner; + private final JFXRippler rippler; + + public TabHeaderContainer(Tab tab) { + this.tab = tab; + + tabText = new Label(); + tabText.textProperty().bind(tab.textProperty()); + tabText.getStyleClass().add("tab-label"); + inner = new BorderPane(); + inner.setCenter(tabText); + inner.getStyleClass().add("tab-container"); + rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT); + rippler.setRipplerFill(ripplerColor); + getChildren().setAll(rippler); + + FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected)); + + this.setOnMouseClicked(event -> { + if (event.getButton() == MouseButton.PRIMARY) { + this.setOpacity(1); + getSkinnable().getSelectionModel().select(tab); + } + }); + } + } + } + + public static class Tab { + private final StringProperty id = new SimpleStringProperty(this, "id"); + private final StringProperty text = new SimpleStringProperty(this, "text"); + private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected"); + + public Tab(String id) { + setId(id); + } + + public Tab(String id, String text) { + setId(id); + setText(text); + } + + public String getId() { + return id.get(); + } + + public StringProperty idProperty() { + return id; + } + + public void setId(String id) { + this.id.set(id); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public void setText(String text) { + this.text.set(text); + } + + public boolean isSelected() { + return selected.get(); + } + + public ReadOnlyBooleanProperty selectedProperty() { + return selected.getReadOnlyProperty(); + } + + private void setSelected(boolean selected) { + this.selected.set(selected); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index c9f35cc7d..99bfa44bc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -35,7 +35,7 @@ public class Decorator extends Control { 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 ObjectProperty state = new SimpleObjectProperty<>(); private final StringProperty drawerTitle = new SimpleStringProperty(); private final ObjectProperty onCloseButtonAction = new SimpleObjectProperty<>(); private final ObjectProperty> onCloseNavButtonAction = new SimpleObjectProperty<>(); @@ -90,16 +90,16 @@ public class Decorator extends Control { this.content.set(content); } - public String getTitle() { - return title.get(); + public DecoratorPage.State getState() { + return state.get(); } - public StringProperty titleProperty() { - return title; + public ObjectProperty stateProperty() { + return state; } - public void setTitle(String title) { - this.title.set(title); + public void setState(DecoratorPage.State state) { + this.state.set(state); } public String getDrawerTitle() { 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..4a29b19b3 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; @@ -70,14 +71,11 @@ public class DecoratorController { private final Decorator 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 Decorator(stage); decorator.setOnCloseButtonAction(Launcher::stopApplication); @@ -220,8 +218,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 +230,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,39 +241,45 @@ 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); } + decorator.canCloseProperty().set(navigator.size() > 2); + if (to instanceof DecoratorPage) { - decorator.drawerTitleProperty().bind(((DecoratorPage) to).titleProperty()); - decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).canForceToClose()); + decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable()); } else { - decorator.drawerTitleProperty().unbind(); - decorator.drawerTitleProperty().set(""); decorator.showCloseAsHomeProperty().set(true); } - decorator.canBackProperty().set(navigator.canGoBack()); - decorator.canCloseProperty().set(navigator.canGoBack()); + // state property should be updated at last. + if (to instanceof DecoratorPage) { + decorator.stateProperty().bind(((DecoratorPage) to).stateProperty()); + } else { + decorator.stateProperty().unbind(); + decorator.stateProperty().set(new DecoratorPage.State("", null, navigator.canGoBack(), false, true)); + } if (to instanceof Region) { Region region = (Region) to; @@ -359,7 +363,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..f74f18c5e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java @@ -0,0 +1,88 @@ +/* + * 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.binding.Bindings; +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); + } + + @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) { + state.bind(Bindings.createObjectBinding(() -> { + State state = ((DecoratorPage) to).stateProperty().get(); + return new State(state.getTitle(), state.getTitleNode(), navigator.canGoBack(), state.isRefreshable(), true); + }, ((DecoratorPage) to).stateProperty())); + } else { + state.unbind(); + state.set(new State("", null, navigator.canGoBack(), false, true)); + } + + 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..255c3e700 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,25 +17,74 @@ */ package org.jackhuang.hmcl.ui.decorator; -import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.scene.Node; import org.jackhuang.hmcl.ui.construct.Navigator; +import org.jackhuang.hmcl.ui.wizard.Refreshable; -public interface DecoratorPage { - ReadOnlyStringProperty titleProperty(); +public interface DecoratorPage extends Refreshable { + ReadOnlyObjectProperty stateProperty(); - default boolean canForceToClose() { + default boolean isPageCloseable() { return false; } - default boolean onClose() { + default boolean back() { return true; } - default void onForceToClose() { + @Override + default void refresh() { + } + + default void closePage() { } default void onDecoratorPageNavigating(Navigator.NavigationEvent event) { ((Node) this).getStyleClass().add("content-background"); } + + class State { + private final String title; + private final Node titleNode; + private final boolean backable; + private final boolean refreshable; + private final boolean animate; + + public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) { + this.title = title; + this.titleNode = titleNode; + this.backable = backable; + this.refreshable = refreshable; + this.animate = animate; + } + + public static State fromTitle(String title) { + return new State(title, null, true, false, true); + } + + public static State fromTitleNode(Node titleNode) { + return new State(null, titleNode, true, false, true); + } + + public String getTitle() { + return title; + } + + public Node getTitleNode() { + return titleNode; + } + + public boolean isBackable() { + return backable; + } + + public boolean isRefreshable() { + return refreshable; + } + + public boolean isAnimate() { + return animate; + } + } } 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..4fcddf285 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 @@ -29,13 +29,18 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.SkinBase; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.util.Lang; public class DecoratorSkin extends SkinBase { @@ -45,6 +50,7 @@ public class DecoratorSkin extends SkinBase { private final BorderPane titleContainer; private final StackPane contentPlaceHolder; private final Stage primaryStage; + private final TransitionPane navBarPane; private double xOffset, yOffset, newX, newY, initX, initY; private boolean allowMove, isDragging; @@ -72,65 +78,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); + // 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); @@ -149,7 +131,17 @@ public class DecoratorSkin extends SkinBase { rectangle.heightProperty().bind(titleContainer.heightProperty().add(100)); titleContainer.setClip(rectangle); { - titleContainer.setCenter(createNavBar(skinnable)); + navBarPane = new TransitionPane(); + FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> { + if (s == null) return; + Node node = createNavBar(skinnable, s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode()); + if (s.isAnimate()) { + navBarPane.setContent(node, ContainerAnimations.FADE.getAnimationProducer()); + } else { + navBarPane.getChildren().setAll(node); + } + }); + titleContainer.setCenter(navBarPane); HBox buttonsContainer = new HBox(); buttonsContainer.setStyle("-fx-background-color: transparent;"); @@ -177,7 +169,7 @@ public class DecoratorSkin extends SkinBase { getChildren().setAll(root); } - private Node createNavBar(Decorator skinnable) { + private Node createNavBar(Decorator skinnable, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) { BorderPane navBar = new BorderPane(); { HBox navLeft = new HBox(); @@ -189,7 +181,7 @@ public class DecoratorSkin extends SkinBase { backNavButton.getStyleClass().add("jfx-decorator-button"); backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty()); - backNavButton.visibleProperty().bind(skinnable.canBackProperty()); + backNavButton.visibleProperty().set(canBack); JFXButton closeNavButton = new JFXButton(); closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1)); @@ -197,48 +189,47 @@ public class DecoratorSkin extends SkinBase { closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty()); - FXUtils.onChangeAndOperate(skinnable.canBackProperty(), (newValue) -> { - navLeft.getChildren().remove(backNavButton); - if (newValue) navLeft.getChildren().add(0, backNavButton); - }); - FXUtils.onChangeAndOperate(skinnable.canCloseProperty(), (newValue) -> { - navLeft.getChildren().remove(closeNavButton); - if (newValue) navLeft.getChildren().add(closeNavButton); - }); - - FXUtils.onChangeAndOperate(skinnable.showCloseAsHomeProperty(), (newValue) -> { - if (newValue) - closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1)); - else - closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1)); - }); + if (canBack) navLeft.getChildren().add(backNavButton); + if (canClose) navLeft.getChildren().add(closeNavButton); + if (showCloseAsHome) + closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1)); + else + closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1)); } navBar.setLeft(navLeft); - VBox navCenter = new VBox(); - navCenter.setAlignment(Pos.CENTER_LEFT); - Label titleLabel = new Label(); - titleLabel.getStyleClass().add("jfx-decorator-title"); - titleLabel.textProperty().bind(skinnable.drawerTitleProperty()); - navCenter.getChildren().setAll(titleLabel); - navBar.setCenter(navCenter); + BorderPane center = new BorderPane(); + if (title != null) { + Label titleLabel = new Label(); + titleLabel.getStyleClass().add("jfx-decorator-title"); + titleLabel.setText(title); + center.setLeft(titleLabel); + BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT); + } + if (titleNode != null) { + center.setCenter(titleNode); + BorderPane.setAlignment(titleNode, Pos.CENTER_LEFT); + BorderPane.setMargin(titleNode, new Insets(0, 0, 0, 8)); + } + navBar.setCenter(center); - HBox navRight = new HBox(); - navRight.setAlignment(Pos.CENTER_RIGHT); - JFXButton refreshNavButton = new JFXButton(); - refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)); - refreshNavButton.getStyleClass().add("jfx-decorator-button"); - refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); - refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty()); - refreshNavButton.visibleProperty().bind(skinnable.canRefreshProperty()); + if (canRefresh) { + HBox navRight = new HBox(); + navRight.setAlignment(Pos.CENTER_RIGHT); + JFXButton refreshNavButton = new JFXButton(); + refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)); + refreshNavButton.getStyleClass().add("jfx-decorator-button"); + refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty()); - Rectangle separator = new Rectangle(); - separator.visibleProperty().bind(refreshNavButton.visibleProperty()); - separator.heightProperty().bind(navBar.heightProperty()); - separator.setFill(Color.GRAY); + Rectangle separator = new Rectangle(); + separator.visibleProperty().bind(refreshNavButton.visibleProperty()); + separator.heightProperty().bind(navBar.heightProperty()); + separator.setFill(Color.GRAY); - navRight.getChildren().setAll(refreshNavButton, separator); - navBar.setRight(navRight); + navRight.getChildren().setAll(refreshNavButton, separator); + navBar.setRight(navRight); + } } return navBar; } 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..1b00cc0a1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java @@ -0,0 +1,68 @@ +/* + * 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.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleBooleanProperty; +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; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public abstract class DecoratorTransitionPage extends Control implements DecoratorPage { + protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n(""))); + private final BooleanProperty refreshable = new SimpleBooleanProperty(false); + 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 boolean isRefreshable() { + return refreshable.get(); + } + + @Override + public BooleanProperty refreshableProperty() { + return refreshable; + } + + public void setRefreshable(boolean refreshable) { + this.refreshable.set(refreshable); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } +} 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..dc385eb20 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,30 @@ 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 + " - "; + String title; if (page instanceof WizardPage) - title.set(prefix + ((WizardPage) page).getTitle()); + title = prefix + ((WizardPage) page).getTitle(); + else + title = ""; + state.set(new State(title, null, true, refreshableProperty().get(), true)); } @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 +100,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 80% 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..15e94b611 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; @@ -41,22 +36,30 @@ import javafx.scene.layout.VBox; import javafx.scene.shape.Rectangle; import javafx.util.Duration; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.game.Version; 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; +import org.jackhuang.hmcl.ui.versions.GameItem; import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.upgrade.UpdateHandler; +import org.jackhuang.hmcl.util.javafx.MappedObservableList; + +import java.util.List; +import java.util.stream.IntStream; import static org.jackhuang.hmcl.ui.FXUtils.SINE; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class MainPage extends StackPane implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", "Hello Minecraft! Launcher " + Metadata.VERSION); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle("Hello Minecraft! Launcher " + Metadata.VERSION)); private final PopupMenu menu = new PopupMenu(); private final JFXPopup popup = new JFXPopup(menu); @@ -64,7 +67,9 @@ public final class MainPage extends StackPane implements DecoratorPage { private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame"); private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate"); private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion"); - private final ObservableList versions = FXCollections.observableArrayList(); + private final ObservableList versions = FXCollections.observableArrayList(); + private final ObservableList versionNodes; + private Profile profile; private StackPane updatePane; private JFXButton menuButton; @@ -112,6 +117,18 @@ public final class MainPage extends StackPane implements DecoratorPage { StackPane launchPane = new StackPane(); launchPane.setMaxWidth(230); launchPane.setMaxHeight(55); + launchPane.setOnScroll(event -> { + int index = IntStream.range(0, versions.size()) + .filter(i -> versions.get(i).getId().equals(getCurrentGame())) + .findFirst().orElse(-1); + if (index < 0) return; + if (event.getDeltaY() > 0) { + index--; + } else { + index++; + } + profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId()); + }); StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT); { JFXButton launchButton = new JFXButton(); @@ -131,7 +148,13 @@ public final class MainPage extends StackPane implements DecoratorPage { launchLabel.setStyle("-fx-font-size: 16px;"); Label currentLabel = new Label(); currentLabel.setStyle("-fx-font-size: 12px;"); - currentLabel.textProperty().bind(currentGameProperty()); + currentLabel.textProperty().bind(Bindings.createStringBinding(() -> { + if (getCurrentGame() == null) { + return i18n("version.empty"); + } else { + return getCurrentGame(); + } + }, currentGameProperty())); graphic.getChildren().setAll(launchLabel, currentLabel); launchButton.setGraphic(graphic); @@ -168,7 +191,12 @@ public final class MainPage extends StackPane implements DecoratorPage { menu.setMaxWidth(545); menu.setAlwaysShowingVBar(true); menu.setOnMouseClicked(e -> popup.hide()); - Bindings.bindContent(menu.getContent(), versions); + versionNodes = MappedObservableList.create(versions, version -> { + Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId())); + node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId())); + return node; + }); + Bindings.bindContent(menu.getContent(), versionNodes); } private void doAnimation(boolean show) { @@ -208,17 +236,9 @@ public final class MainPage extends StackPane implements DecoratorPage { showUpdate.set(false); } - public String getTitle() { - return title.get(); - } - @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); - } - - public void setTitle(String title) { - this.title.set(title); + public ReadOnlyObjectWrapper stateProperty() { + return state; } public String getCurrentGame() { @@ -257,7 +277,9 @@ public final class MainPage extends StackPane implements DecoratorPage { this.latestVersion.set(latestVersion); } - public ObservableList getVersions() { - return versions; + public void initVersions(Profile profile, List versions) { + FXUtils.checkFxUserThread(); + this.profile = profile; + this.versions.setAll(versions); } } 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..16c7a13e2 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -0,0 +1,269 @@ +/* + * 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.geometry.Insets; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +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.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.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 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(), mainPage::setCurrentGame); + 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()))) + .collect(Collectors.toList()); + runInFX(() -> { + if (profile == Profiles.getSelectedProfile()) + mainPage.initVersions(profile, children); + }); + }); + this.mainPage = mainPage; + } + 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(); + + { + StackPane drawerContainer = new StackPane(); + FXUtils.setLimitWidth(drawerContainer, 200); + drawerContainer.getStyleClass().add("gray-background"); + drawerContainer.getChildren().setAll(sideBar); + FXUtils.setOverflowHidden(drawerContainer, 8); + + StackPane wrapper = new StackPane(drawerContainer); + wrapper.setPadding(new Insets(4, 0, 4, 4)); + root.setLeft(wrapper); + } + + { + control.navigator.getStyleClass().add("jfx-decorator-content-container"); + control.navigator.init(getSkinnable().getMainPage()); + FXUtils.setOverflowHidden(control.navigator, 8); + StackPane wrapper = new StackPane(control.navigator); + wrapper.setPadding(new Insets(4)); + root.setCenter(wrapper); + } + + 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 95% 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..211679600 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; @@ -24,13 +24,15 @@ import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.binding.When; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.scene.control.ToggleGroup; 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; @@ -65,7 +67,7 @@ import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelected import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; public final class SettingsPage extends SettingsView implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("settings.launcher")); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings.launcher"))); private InvalidationListener updateListener; @@ -198,17 +200,9 @@ public final class SettingsPage extends SettingsView implements DecoratorPage { // ==== } - public String getTitle() { - return title.get(); - } - @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); - } - - public void setTitle(String title) { - this.title.set(title); + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } @Override 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/profile/ProfileList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java index a29a8c850..4e936054d 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 @@ -29,7 +29,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; public class ProfileList extends ListPage implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(i18n("profile.manage")); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("profile.manage"))); private final ListProperty profiles = new SimpleListProperty<>(FXCollections.observableArrayList()); private ObjectProperty selectedProfile; @@ -52,7 +52,7 @@ public class ProfileList extends ListPage implements DecoratorP } @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } } 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 405cdb1fc..e5ad0caa7 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 @@ -21,8 +21,8 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXTextField; import com.jfoenix.validation.base.ValidatorBase; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; @@ -41,7 +41,7 @@ import java.util.Optional; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class ProfilePage extends StackPane implements DecoratorPage { - private final ReadOnlyStringWrapper title; + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final StringProperty location; private final Profile profile; @@ -57,8 +57,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage { this.profile = profile; String profileDisplayName = Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse(""); - title = new ReadOnlyStringWrapper(this, "title", - profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName); + state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName)); location = new SimpleStringProperty(this, "location", Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft")); @@ -111,17 +110,9 @@ public final class ProfilePage extends StackPane implements DecoratorPage { fireEvent(new PageCloseEvent()); } - public String getTitle() { - return title.get(); - } - @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); - } - - public void setTitle(String title) { - this.title.set(title); + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } public String getLocation() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java index 278c60c59..e66b512d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java @@ -17,8 +17,8 @@ */ package org.jackhuang.hmcl.ui.versions; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ObservableList; import javafx.scene.control.Skin; import javafx.stage.FileChooser; @@ -43,7 +43,7 @@ import java.util.logging.Level; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DatapackListPage extends ListPageBase implements DecoratorPage { - private final StringProperty title = new SimpleStringProperty(); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final Path worldDir; private final Datapack datapack; @@ -52,7 +52,7 @@ public class DatapackListPage extends ListPageBase stateProperty() { + return state.getReadOnlyProperty(); } public void add() { 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 6466402c2..14eff58ad 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 @@ -18,10 +18,11 @@ package org.jackhuang.hmcl.ui.versions; import javafx.application.Platform; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.scene.Node; import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; import org.jackhuang.hmcl.event.EventBus; import org.jackhuang.hmcl.event.RefreshingVersionsEvent; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -33,10 +34,9 @@ import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider; -import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.versioning.VersionNumber; -import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; @@ -46,7 +46,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameList extends ListPageBase implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage")); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage"))); private ToggleGroup toggleGroup; @@ -123,24 +123,29 @@ public class GameList extends ListPageBase implements DecoratorPag } @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } private class GameListSkin extends ToolbarListPageSkin { public GameListSkin() { super(GameList.this); + + HBox hbox = new HBox( + createToolbarButton(i18n("install.new_game"), SVG::plus, GameList.this::addNewGame), + createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList.this::importModpack), + createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList.this::refresh), + createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, GameList.this::modifyGlobalGameSettings) + ); + hbox.setPickOnBounds(false); + + state.set(new State(i18n("version.manage"), hbox, true, false, true)); } @Override protected List initializeToolbar(GameList skinnable) { - return Arrays.asList( - createToolbarButton(i18n("install.new_game"), SVG::plus, skinnable::addNewGame), - createToolbarButton(i18n("install.modpack"), SVG::importIcon, skinnable::importModpack), - createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh), - createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, skinnable::modifyGlobalGameSettings) - ); + return Collections.emptyList(); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java index 26ed5f975..1b385abd2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -91,7 +91,7 @@ public class GameListItemSkin extends SkinBase { right.getChildren().add(btnManage); root.setRight(right); - root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); + root.setStyle("-fx-background-color: white; -fx-background-radius: 4; -fx-padding: 8 8 8 0;"); JFXDepthManager.setDepth(root, 1); getChildren().setAll(root); 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..6bea32ef9 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 @@ -17,7 +17,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; @@ -32,15 +32,18 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.util.Logging; 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; @@ -50,11 +53,12 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class ModListPage extends ListPageBase { private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); - private JFXTabPane parentTab; + private TabHeader.Tab tab; private ModManager modManager; private LibraryAnalyzer libraryAnalyzer; - public ModListPage() { + public ModListPage(TabHeader.Tab tab) { + this.tab = tab; FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> { mods.forEach(it -> { @@ -77,28 +81,34 @@ 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) + getProperties().put(ModListPage.class, FXUtils.onWeakChangeAndOperate(tab.selectedProperty(), newValue -> { + if (newValue) itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList())); - }); - }).start(); + })); + else + getProperties().remove(ModListPage.class); + }, Platform::runLater); } public void add() { @@ -134,10 +144,6 @@ public final class ModListPage extends ListPageBase selectedItems) { try { modManager.removeMods(selectedItems.stream() 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..8bfbd1696 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 @@ -18,111 +18,101 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; -import com.jfoenix.controls.JFXTabPane; import javafx.application.Platform; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.fxml.FXML; -import javafx.scene.control.Tab; +import javafx.beans.property.*; +import javafx.geometry.Pos; +import javafx.scene.control.Control; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; 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.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -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.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.io.FileUtils; +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.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class VersionPage extends StackPane implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", null); - - @FXML - private VersionSettingsPage versionSettings; - @FXML - private Tab modTab; - @FXML - private ModListPage mod; - @FXML - private InstallerListPage installer; - @FXML - private WorldListPage world; - @FXML - private JFXButton btnBrowseMenu; - @FXML - private JFXButton btnDelete; - @FXML - private JFXButton btnManagementMenu; - @FXML - private JFXButton btnExport; - @FXML - private JFXButton btnTestGame; - @FXML - private StackPane rootPane; - @FXML - private StackPane contentPane; - @FXML - private JFXTabPane tabPane; - - private final JFXPopup browsePopup; - private final JFXPopup managementPopup; +public class VersionPage extends Control implements DecoratorPage { + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); + private final BooleanProperty loading = new SimpleBooleanProperty(); + private final JFXListView listView = new JFXListView<>(); + private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab("versionSettingsTab"); + private final VersionSettingsPage versionSettingsPage = new VersionSettingsPage(); + private final TabHeader.Tab modListTab = new TabHeader.Tab("modListTab"); + private final ModListPage modListPage = new ModListPage(modListTab); + private final TabHeader.Tab installerListTab = new TabHeader.Tab("installerListTab"); + private final InstallerListPage installerListPage = new InstallerListPage(); + private final TabHeader.Tab worldListTab = new TabHeader.Tab("worldList"); + private final WorldListPage worldListPage = new WorldListPage(); + private final TransitionPane transitionPane = new TransitionPane(); + private final ObjectProperty selectedTab = new SimpleObjectProperty<>(); private Profile profile; private String version; { - FXUtils.loadFXML(this, "/assets/fxml/version/version.fxml"); + Profiles.registerVersionsListener(this::loadVersions); - PopupMenu browseList = new PopupMenu(); - browsePopup = new JFXPopup(browseList); - browseList.getContent().setAll( - new IconedMenuItem(null, i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> onBrowse(""), browsePopup)), - new IconedMenuItem(null, i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> onBrowse("mods"), browsePopup)), - new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> onBrowse("config"), browsePopup)), - new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> onBrowse("resourcepacks"), browsePopup)), - new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> onBrowse("screenshots"), browsePopup)), - new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> onBrowse("saves"), browsePopup)) - ); + listView.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> { + loadVersion(newValue, profile); + }); - PopupMenu managementList = new PopupMenu(); - managementPopup = new JFXPopup(managementList); - managementList.getContent().setAll( - new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(() -> Versions.updateGameAssets(profile, version), managementPopup)), - new IconedMenuItem(null, i18n("version.manage.remove_libraries"), FXUtils.withJFXPopupClosing(() -> FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")), managementPopup)), - new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(() -> Versions.cleanVersion(profile, version), managementPopup)).addTooltip(i18n("version.manage.clean.tooltip")) - ); - - FXUtils.installFastTooltip(btnDelete, i18n("version.manage.remove")); - FXUtils.installFastTooltip(btnBrowseMenu, i18n("settings.game.exploration")); - FXUtils.installFastTooltip(btnManagementMenu, i18n("settings.game.management")); - FXUtils.installFastTooltip(btnExport, i18n("modpack.export")); - - 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); } - public void load(String id, Profile profile) { - this.version = id; + 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); + this.version = version; this.profile = profile; - title.set(i18n("version.manage.manage") + " - " + id); + versionSettingsPage.loadVersion(profile, version); + loading.set(true); - versionSettings.loadVersion(profile, id); - mod.setParentTab(tabPane); - modTab.setUserData(mod); - mod.loadVersion(profile, id); - installer.loadVersion(profile, id); - world.loadVersion(profile, id); + CompletableFuture.allOf( + modListPage.loadVersion(profile, version), + installerListPage.loadVersion(profile, version), + worldListPage.loadVersion(profile, version)) + .whenCompleteAsync((result, exception) -> loading.set(false), Platform::runLater); } private void onNavigated(Navigator.NavigationEvent event) { @@ -137,38 +127,164 @@ public final class VersionPage extends StackPane implements DecoratorPage { return; } - load(this.version, this.profile); - } - - @FXML - private void onTestGame() { - Versions.testGame(profile, version); - } - - @FXML - private void onBrowseMenu() { - browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, btnBrowseMenu.getHeight()); - } - - @FXML - private void onManagementMenu() { - managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, btnManagementMenu.getHeight()); + loadVersion(this.version, this.profile); } private void onBrowse(String sub) { FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub)); } - public String getTitle() { - return title.get(); + private void redownloadAssetIndex() { + Versions.updateGameAssets(profile, version); + } + + private void clearLibraries() { + FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")); + } + + private void clearJunkFiles() { + Versions.cleanVersion(profile, version); + } + + private void testGame() { + Versions.testGame(profile, version); } @Override - public ReadOnlyStringProperty titleProperty() { - return title.getReadOnlyProperty(); + protected Skin createDefaultSkin() { + return new Skin(this); } - public void setTitle(String title) { - this.title.set(title); + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.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(VersionPage 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); + } + + TabHeader tabPane = new TabHeader(); + tabPane.setPickOnBounds(false); + tabPane.getStyleClass().add("jfx-decorator-tab"); + control.versionSettingsTab.setText(i18n("settings")); + control.modListTab.setText(i18n("mods")); + control.installerListTab.setText(i18n("settings.tabs.installers")); + control.worldListTab.setText(i18n("world")); + tabPane.getTabs().setAll( + control.versionSettingsTab, + control.modListTab, + control.installerListTab, + control.worldListTab); + control.selectedTab.bind(tabPane.getSelectionModel().selectedItemProperty()); + FXUtils.onChangeAndOperate(tabPane.getSelectionModel().selectedItemProperty(), newValue -> { + if (control.versionSettingsTab.equals(newValue)) { + control.transitionPane.setContent(control.versionSettingsPage, ContainerAnimations.FADE.getAnimationProducer()); + } else if (control.modListTab.equals(newValue)) { + control.transitionPane.setContent(control.modListPage, ContainerAnimations.FADE.getAnimationProducer()); + } else if (control.installerListTab.equals(newValue)) { + control.transitionPane.setContent(control.installerListPage, ContainerAnimations.FADE.getAnimationProducer()); + } else if (control.worldListTab.equals(newValue)) { + control.transitionPane.setContent(control.worldListPage, ContainerAnimations.FADE.getAnimationProducer()); + } + }); + + HBox toolBar = new HBox(); + toolBar.setAlignment(Pos.TOP_RIGHT); + toolBar.setPickOnBounds(false); + { + PopupMenu browseList = new PopupMenu(); + JFXPopup browsePopup = new JFXPopup(browseList); + browseList.getContent().setAll( + new IconedMenuItem(null, i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> control.onBrowse(""), browsePopup)), + new IconedMenuItem(null, i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("mods"), browsePopup)), + new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("config"), browsePopup)), + new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("resourcepacks"), browsePopup)), + new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("screenshots"), browsePopup)), + new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("saves"), browsePopup)) + ); + + PopupMenu managementList = new PopupMenu(); + JFXPopup managementPopup = new JFXPopup(managementList); + managementList.getContent().setAll( + new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(control::redownloadAssetIndex, managementPopup)), + new IconedMenuItem(null, i18n("version.manage.remove_libraries"), FXUtils.withJFXPopupClosing(control::clearLibraries, managementPopup)), + new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(control::clearJunkFiles, managementPopup)).addTooltip(i18n("version.manage.clean.tooltip")) + ); + + JFXButton testGameButton = new JFXButton(); + FXUtils.setLimitWidth(testGameButton, 40); + FXUtils.setLimitHeight(testGameButton, 40); + testGameButton.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20)); + testGameButton.getStyleClass().add("jfx-decorator-button"); + testGameButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + testGameButton.setOnAction(event -> control.testGame()); + FXUtils.installFastTooltip(testGameButton, i18n("version.launch.test")); + + JFXButton browseMenuButton = new JFXButton(); + FXUtils.setLimitWidth(browseMenuButton, 40); + FXUtils.setLimitHeight(browseMenuButton, 40); + browseMenuButton.setGraphic(SVG.folderOpen(Theme.whiteFillBinding(), 20, 20)); + browseMenuButton.getStyleClass().add("jfx-decorator-button"); + browseMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + browseMenuButton.setOnAction(event -> browsePopup.show(browseMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, browseMenuButton.getHeight())); + FXUtils.installFastTooltip(browseMenuButton, i18n("settings.game.exploration")); + + JFXButton managementMenuButton = new JFXButton(); + FXUtils.setLimitWidth(managementMenuButton, 40); + FXUtils.setLimitHeight(managementMenuButton, 40);; + managementMenuButton.setGraphic(SVG.wrench(Theme.whiteFillBinding(), 20, 20)); + managementMenuButton.getStyleClass().add("jfx-decorator-button"); + managementMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + managementMenuButton.setOnAction(event -> managementPopup.show(managementMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, managementMenuButton.getHeight())); + FXUtils.installFastTooltip(managementMenuButton, i18n("settings.game.management")); + + toolBar.getChildren().setAll(testGameButton, browseMenuButton, managementMenuButton); + } + + BorderPane titleBar = new BorderPane(); + titleBar.setLeft(tabPane); + titleBar.setRight(toolBar); + control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true)); + + root.setCenter(control.transitionPane); + + spinnerPane.loadingProperty().bind(control.loading); + spinnerPane.setContent(root); + getChildren().setAll(spinnerPane); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 3450b8745..f0258bc67 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -24,8 +24,8 @@ import com.jfoenix.controls.JFXToggleButton; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; @@ -63,7 +63,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class VersionSettingsPage extends StackPane implements DecoratorPage { - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(new State("", null, false, false, false)); private VersionSetting lastVersionSetting = null; private Profile profile; @@ -163,7 +163,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag rootPane.getChildren().remove(iconPickerItemWrapper); rootPane.getChildren().remove(settingsTypePane); chkEnableSpecificSettings.setSelected(true); - title.set(Profiles.getProfileDisplayName(profile) + " - " + i18n("settings.type.global.manage")); + state.set(State.fromTitle(Profiles.getProfileDisplayName(profile) + " - " + i18n("settings.type.global.manage"))); } VersionSetting versionSetting = profile.getVersionSetting(versionId); @@ -328,7 +328,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag } @Override - public ReadOnlyStringProperty titleProperty() { - return title; + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); } } 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/css/root.css b/HMCL/src/main/resources/assets/css/root.css index cbaf23a66..0f0a344b2 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -18,6 +18,20 @@ .root { } +.scroll-bar { + -fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin"; +} + +.scroll-bar .track { + -fx-fill: transparent; +} + +.scroll-bar .thumb { + -fx-fill: rgba(255, 255, 255, 0.5); + -fx-arc-width: 5px; + -fx-arc-height: 5px; +} + .disabled Label { -fx-text-fill: rgba(0, 0, 0, 0.5); } @@ -353,7 +367,7 @@ } .jfx-tool-bar HBox { - -fx-alignment: center; + -fx-alignment: center-left; -fx-padding: 0.0 5.0; } @@ -1024,6 +1038,11 @@ -fx-font-size: 14; } +.jfx-decorator-tab .tab-label { + -fx-text-fill: -fx-base-text-fill; + -fx-font-size: 14; +} + .resize-border { -fx-border-color: -fx-base-color; -fx-border-width: 0 2 2 2; 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 @@ + - + diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 9e2df62a1..16d552f41 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -58,6 +58,20 @@ public final class Lang { return Collections.unmodifiableList(Arrays.asList(elements)); } + public static > T clamp(T min, T val, T max) { + if (val.compareTo(min) < 0) return min; + else if (val.compareTo(max) > 0) return max; + else return val; + } + + public static double clamp(double min, double val, double max) { + return Math.max(min, Math.min(val, max)); + } + + public static int clamp(int min, int val, int max) { + return Math.max(min, Math.min(val, max)); + } + public static boolean test(ExceptionalRunnable r) { try { r.run();