alt: initial UI refactor

This commit is contained in:
huanghongxun 2020-02-25 09:36:18 +08:00
parent 1ca4045a2f
commit b117769711
31 changed files with 948 additions and 479 deletions

View File

@ -23,58 +23,36 @@ import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.VersionRootPage;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
private static Scene scene;
private static Stage stage;
private static MainPage mainPage = null;
private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null;
private static GameList gameListPage = null;
private static AccountList accountListPage = null;
private static ProfileList profileListPage = null;
private static VersionRootPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController;
private static RootPage rootPage;
private static DecoratorController decorator;
public static Scene getScene() {
@ -86,53 +64,19 @@ public final class Controllers {
}
// FXThread
public static SettingsPage getSettingsPage() {
if (settingsPage == null)
settingsPage = new SettingsPage();
return settingsPage;
}
// FXThread
public static GameList getGameListPage() {
if (gameListPage == null) {
gameListPage = new GameList();
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
}
return gameListPage;
}
// FXThread
public static AccountList getAccountListPage() {
if (accountListPage == null) {
AccountList accountListPage = new AccountList();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
Controllers.accountListPage = accountListPage;
}
return accountListPage;
}
// FXThread
public static ProfileList getProfileListPage() {
if (profileListPage == null) {
ProfileList profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
Controllers.profileListPage = profileListPage;
}
return profileListPage;
}
// FXThread
public static VersionPage getVersionPage() {
public static VersionRootPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
versionPage = new VersionRootPage();
return versionPage;
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
}
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
@ -145,51 +89,6 @@ public final class Controllers {
return decorator;
}
public static MainPage getMainPage() {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
mainPage.setCurrentGame(version);
} else {
mainPage.setCurrentGame(i18n("version.empty"));
}
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
List<Node> 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;
}
}

View File

@ -1,133 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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();
});
}
}

View File

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

View File

@ -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<KeyFrame> animate(AnimationHandler handler);
@Nullable AnimationProducer opposite();
}

View File

@ -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<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
this.animationProducer = new AnimationProducer() {
@ -155,10 +189,30 @@ public enum ContainerAnimations {
public List<KeyFrame> 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;
}
}

View File

@ -21,29 +21,18 @@ import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
public final class TransitionHandler implements AnimationHandler {
private final StackPane view;
public class TransitionPane extends StackPane implements AnimationHandler {
private Timeline animation;
private Duration duration;
private Node previousNode, currentNode;
/**
* @param view A stack pane that contains another control that is {@link Parent}
*/
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
{
currentNode = getChildren().stream().findFirst().orElse(null);
FXUtils.setOverflowHidden(this, true);
}
@Override
@ -58,7 +47,7 @@ public final class TransitionHandler implements AnimationHandler {
@Override
public StackPane getCurrentRoot() {
return view;
return this;
}
@Override
@ -86,8 +75,8 @@ public final class TransitionHandler implements AnimationHandler {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(transition.animate(this));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
view.setMouseTransparent(false);
view.getChildren().remove(previousNode);
setMouseTransparent(false);
getChildren().remove(previousNode);
}));
nowAnimation.play();
animation = nowAnimation;
@ -95,7 +84,7 @@ public final class TransitionHandler implements AnimationHandler {
}
private void updateContent(Node newView) {
if (view.getWidth() > 0 && view.getHeight() > 0) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = EMPTY_PANE;
@ -105,11 +94,11 @@ public final class TransitionHandler implements AnimationHandler {
if (previousNode == newView)
previousNode = EMPTY_PANE;
view.setMouseTransparent(true);
setMouseTransparent(true);
currentNode = newView;
view.getChildren().setAll(previousNode, currentNode);
getChildren().setAll(previousNode, currentNode);
}
private final StackPane EMPTY_PANE = new StackPane();

View File

@ -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<Node> stack = new Stack<>();
private final TransitionHandler animationHandler = new TransitionHandler(this);
private boolean initialized = false;
public void init(Node init) {
@ -50,7 +49,7 @@ public class Navigator extends StackPane {
initialized = true;
}
public void navigate(Node node) {
public void navigate(Node node, AnimationProducer animationProducer) {
FXUtils.checkFxUserThread();
if (!initialized)
@ -68,7 +67,8 @@ public class Navigator extends StackPane {
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
node.getProperties().put("hmcl.navigator.animation", animationProducer);
setContent(node, animationProducer);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
@ -110,7 +110,12 @@ public class Navigator extends StackPane {
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
Object obj = from.getProperties().get("hmcl.navigator.animation");
if (obj instanceof AnimationProducer) {
setContent(node, (AnimationProducer) obj);
} else {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
}
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
@ -128,8 +133,8 @@ public class Navigator extends StackPane {
return stack.size() > 1;
}
private void setContent(Node content) {
animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer());
public void setContent(Node content, AnimationProducer animationProducer) {
super.setContent(content, animationProducer);
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
@ -179,14 +184,21 @@ public class Navigator extends StackPane {
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
private final Navigator source;
private final Node node;
public NavigationEvent(Object source, Node target, EventType<? extends Event> eventType) {
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.source = source;
this.node = target;
}
@Override
public Navigator getSource() {
return source;
}
public Node getNode() {
return node;
}

View File

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

View File

@ -42,6 +42,7 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.Navigator;
@ -220,8 +221,8 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
if (page.canForceToClose()) {
page.onForceToClose();
if (page.isPageCloseable()) {
page.closePage();
return;
}
}
@ -232,7 +233,7 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
if (page.onClose())
if (page.back())
navigator.close();
} else {
navigator.close();
@ -243,23 +244,25 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof Refreshable) {
Refreshable refreshable = (Refreshable) navigator.getCurrentPage();
if (refreshable.canRefreshProperty().get())
if (refreshable.refreshableProperty().get())
refreshable.refresh();
}
}
private void onNavigating(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node from = event.getNode();
if (from instanceof DecoratorPage)
((DecoratorPage) from).onClose();
((DecoratorPage) from).back();
}
private void onNavigated(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node to = event.getNode();
if (to instanceof Refreshable) {
decorator.canRefreshProperty().bind(((Refreshable) to).canRefreshProperty());
decorator.canRefreshProperty().bind(((Refreshable) to).refreshableProperty());
} else {
decorator.canRefreshProperty().unbind();
decorator.canRefreshProperty().set(false);
@ -267,14 +270,17 @@ public class DecoratorController {
if (to instanceof DecoratorPage) {
decorator.drawerTitleProperty().bind(((DecoratorPage) to).titleProperty());
decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).canForceToClose());
decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable());
decorator.canBackProperty().bind(Bindings.createBooleanBinding(() -> navigator.canGoBack() || ((DecoratorPage) to).backableProperty().get(),
((DecoratorPage) to).backableProperty()));
} else {
decorator.drawerTitleProperty().unbind();
decorator.drawerTitleProperty().set("");
decorator.showCloseAsHomeProperty().set(true);
decorator.canBackProperty().unbind();
decorator.canBackProperty().setValue(navigator.canGoBack());
}
decorator.canBackProperty().set(navigator.canGoBack());
decorator.canCloseProperty().set(navigator.canGoBack());
if (to instanceof Region) {
@ -359,7 +365,7 @@ public class DecoratorController {
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category));
getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
}
// ==== Authlib Injector DnD ====

View File

@ -0,0 +1,87 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage {
protected final Navigator navigator = new Navigator();
{
this.navigator.setOnNavigating(this::onNavigating);
this.navigator.setOnNavigated(this::onNavigated);
}
@Override
protected void navigate(Node page, AnimationProducer animationProducer) {
navigator.navigate(page, animationProducer);
setRefreshable(page instanceof Refreshable);
}
@Override
public boolean back() {
if (navigator.canGoBack()) {
navigator.close();
return false;
} else {
return true;
}
}
private void onNavigating(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node from = event.getNode();
if (from instanceof DecoratorPage)
((DecoratorPage) from).back();
}
private void onNavigated(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node to = event.getNode();
if (to instanceof Refreshable) {
refreshableProperty().bind(((Refreshable) to).refreshableProperty());
} else {
refreshableProperty().unbind();
refreshableProperty().set(false);
}
if (to instanceof DecoratorPage) {
titleProperty().bind(((DecoratorPage) to).titleProperty());
} else {
titleProperty().unbind();
titleProperty().set("");
}
setBackable(navigator.canGoBack());
if (to instanceof Region) {
Region region = (Region) to;
// Let root pane fix window size.
StackPane parent = (StackPane) region.getParent();
region.prefWidthProperty().bind(parent.widthProperty());
region.prefHeightProperty().bind(parent.heightProperty());
}
}
}

View File

@ -17,22 +17,28 @@
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import org.jackhuang.hmcl.ui.construct.Navigator;
public interface DecoratorPage {
ReadOnlyStringProperty titleProperty();
default boolean canForceToClose() {
default boolean isPageCloseable() {
return false;
}
default boolean onClose() {
default boolean back() {
return true;
}
default void onForceToClose() {
default BooleanProperty backableProperty() {
return new SimpleBooleanProperty(true);
}
default void closePage() {
}
default void onDecoratorPageNavigating(Navigator.NavigationEvent event) {

View File

@ -72,65 +72,41 @@ public class DecoratorSkin extends SkinBase<Decorator> {
root.setMaxWidth(Region.USE_PREF_SIZE);
root.setMinWidth(Region.USE_PREF_SIZE);
StackPane drawerWrapper = new StackPane();
skinnable.setDrawerWrapper(drawerWrapper);
drawerWrapper.getStyleClass().add("jfx-decorator-drawer");
drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty());
FXUtils.setOverflowHidden(drawerWrapper, true);
// center node with a container node in bottom layer and a "welcome" layer at the top layer.
StackPane container = new StackPane();
skinnable.setDrawerWrapper(container);
container.getStyleClass().add("jfx-decorator-drawer");
container.backgroundProperty().bind(skinnable.backgroundProperty());
FXUtils.setOverflowHidden(container, true);
// bottom layer
{
BorderPane drawer = new BorderPane();
contentPlaceHolder = new StackPane();
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
{
BorderPane leftRootPane = new BorderPane();
FXUtils.setLimitWidth(leftRootPane, 200);
leftRootPane.getStyleClass().add("jfx-decorator-content-container");
StackPane drawerContainer = new StackPane();
drawerContainer.getStyleClass().add("gray-background");
Bindings.bindContent(drawerContainer.getChildren(), skinnable.drawerProperty());
leftRootPane.setCenter(drawerContainer);
Rectangle separator = new Rectangle();
separator.heightProperty().bind(drawer.heightProperty());
separator.setWidth(1);
separator.setFill(Color.GRAY);
leftRootPane.setRight(separator);
drawer.setLeft(leftRootPane);
}
{
contentPlaceHolder = new StackPane();
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
FXUtils.setOverflowHidden(contentPlaceHolder, true);
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
drawer.setCenter(contentPlaceHolder);
}
drawerWrapper.getChildren().add(drawer);
container.getChildren().add(contentPlaceHolder);
}
// top layer for welcome and hint
{
StackPane container = new StackPane();
Bindings.bindContent(container.getChildren(), skinnable.containerProperty());
StackPane floatLayer = new StackPane();
Bindings.bindContent(floatLayer.getChildren(), skinnable.containerProperty());
ListChangeListener<Node> 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);

View File

@ -0,0 +1,93 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
public abstract class DecoratorTransitionPage extends Control implements DecoratorPage, Refreshable {
private final StringProperty title = new SimpleStringProperty();
private final BooleanProperty refreshable = new SimpleBooleanProperty(false);
private final BooleanProperty backable = new SimpleBooleanProperty();
private Node currentPage;
protected final TransitionPane transitionPane = new TransitionPane();
protected void navigate(Node page, AnimationProducer animation) {
transitionPane.setContent(currentPage = page, animation);
refreshable.setValue(page instanceof Refreshable);
}
@Override
protected abstract Skin<?> createDefaultSkin();
protected Node getCurrentPage() {
return currentPage;
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public boolean isRefreshable() {
return refreshable.get();
}
@Override
public void refresh() {
// empty implementation for default
}
@Override
public BooleanProperty refreshableProperty() {
return refreshable;
}
public void setRefreshable(boolean refreshable) {
this.refreshable.set(refreshable);
}
public boolean isBackable() {
return backable.get();
}
@Override
public BooleanProperty backableProperty() {
return backable;
}
public void setBackable(boolean backable) {
this.backable.set(backable);
}
}

View File

@ -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<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private final String category;
private Node nowPage;
public DecoratorWizardDisplayer(WizardProvider provider) {
this(provider, null);
}
@ -56,16 +45,6 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
}
@Override
public StringProperty titleProperty() {
return title;
}
@Override
public BooleanProperty canRefreshProperty() {
return canRefresh;
}
@Override
public WizardController getWizardController() {
return wizardController;
@ -88,30 +67,26 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
nowPage = page;
transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer());
canRefresh.set(page instanceof Refreshable);
navigate(page, nav.getAnimation().getAnimationProducer());
String prefix = category == null ? "" : category + " - ";
if (page instanceof WizardPage)
title.set(prefix + ((WizardPage) page).getTitle());
setTitle(prefix + ((WizardPage) page).getTitle());
}
@Override
public boolean canForceToClose() {
public boolean isPageCloseable() {
return true;
}
@Override
public void onForceToClose() {
public void closePage() {
wizardController.onCancel();
}
@Override
public boolean onClose() {
public boolean back() {
if (wizardController.canPrev()) {
wizardController.onPrev(true);
return false;
@ -121,6 +96,20 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD
@Override
public void refresh() {
((Refreshable) nowPage).refresh();
((Refreshable) getCurrentPage()).refresh();
}
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
}
private static class Skin extends SkinBase<DecoratorWizardDisplayer> {
protected Skin(DecoratorWizardDisplayer control) {
super(control);
getChildren().setAll(control.transitionPane);
}
}
}

View File

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

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
@ -23,12 +23,7 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
@ -44,6 +39,8 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;

View File

@ -0,0 +1,287 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.decorator.DecoratorNavigatorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorNavigatorPage {
private MainPage mainPage = null;
private SettingsPage settingsPage = null;
private GameList gameListPage = null;
private AccountList accountListPage = null;
private ProfileList profileListPage = null;
public RootPage() {
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
}
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
}
private MainPage getMainPage() {
if (mainPage == null) {
mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
mainPage.setCurrentGame(version);
} else {
mainPage.setCurrentGame(i18n("version.empty"));
}
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
return node;
})
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
});
}
return mainPage;
}
private SettingsPage getSettingsPage() {
if (settingsPage == null)
settingsPage = new SettingsPage();
return settingsPage;
}
private GameList getGameListPage() {
if (gameListPage == null) {
gameListPage = new GameList();
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
}
return gameListPage;
}
private AccountList getAccountListPage() {
if (accountListPage == null) {
accountListPage = new AccountList();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
}
return accountListPage;
}
private ProfileList getProfileListPage() {
if (profileListPage == null) {
profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
}
return profileListPage;
}
private static class Skin extends SkinBase<RootPage> {
protected Skin(RootPage control) {
super(control);
// first item in left sidebar
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getAccountListPage(), ContainerAnimations.FADE.getAnimationProducer()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
// second item in left sidebar
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer());
} else {
Versions.modifyGameSettings(profile, version);
}
});
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer()));
// forth item in left sidebar
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getProfileListPage(), ContainerAnimations.FADE.getAnimationProducer()));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getSettingsPage(), ContainerAnimations.FADE.getAnimationProducer()));
// the left sidebar
AdvancedListBox sideBar = new AdvancedListBox()
.startCategory(i18n("account").toUpperCase())
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
// the root page, with the sidebar in left, navigator in center.
BorderPane root = new BorderPane();
{
BorderPane leftRootPane = new BorderPane();
FXUtils.setLimitWidth(leftRootPane, 200);
StackPane drawerContainer = new StackPane();
drawerContainer.getStyleClass().add("gray-background");
drawerContainer.getChildren().setAll(sideBar);
leftRootPane.setCenter(drawerContainer);
Rectangle separator = new Rectangle();
separator.heightProperty().bind(root.heightProperty());
separator.setWidth(1);
separator.setFill(Color.GRAY);
leftRootPane.setRight(separator);
root.setLeft(leftRootPane);
}
{
control.navigator.getStyleClass().add("jfx-decorator-content-container");
control.navigator.init(getSkinnable().getMainPage());
root.setCenter(control.navigator);
}
getChildren().setAll(root);
}
}
// ==== Accounts ====
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
Platform.runLater(this::addNewAccount);
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
// ====
private boolean checkedModpack = false;
private void onRefreshedVersions(HMCLGameRepository repository) {
runInFX(() -> {
if (!checkedModpack) {
checkedModpack = true;
if (repository.getVersionCount() == 0) {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.withRunAsync(Schedulers.javafx(), this::checkAccount).executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"));
executor.start();
}).start();
}
}
}
checkAccount();
});
}
}

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.effects.JFXDepthManager;
@ -31,6 +31,8 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.Validator;

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -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<InstallerItem> {
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<String, Consumer<InstallerItem>> removeAction = libraryId -> x -> {
profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::save)
@ -100,7 +97,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
else
itemsProperty().add(new InstallerItem(title, libraryVersion, null, action));
}
}).start();
}, Platform::runLater);
}
public void installOnline() {

View File

@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
@ -37,10 +38,12 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -77,28 +80,32 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
loadMods(modManager);
}
public void loadVersion(Profile profile, String id) {
public CompletableFuture<?> loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
modded.set(libraryAnalyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
return loadMods(profile.getRepository().getModManager(id));
}
private void loadMods(ModManager modManager) {
private CompletableFuture<?> loadMods(ModManager modManager) {
this.modManager = modManager;
Task.supplyAsync(() -> {
synchronized (ModListPage.this) {
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new LinkedList<>(modManager.getMods());
return CompletableFuture.supplyAsync(() -> {
try {
synchronized (ModListPage.this) {
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new LinkedList<>(modManager.getMods());
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}).whenComplete(Schedulers.javafx(), (list, exception) -> {
}).whenCompleteAsync((list, exception) -> {
loadingProperty().set(false);
if (exception == null)
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModListPage.this)
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList()));
});
}).start();
}, Platform::runLater);
}
public void add() {

View File

@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
@ -34,15 +36,16 @@ import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionPage extends StackPane implements DecoratorPage {
public final class VersionPage extends StackPane {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", null);
private final ReadOnlyBooleanWrapper loading = new ReadOnlyBooleanWrapper(this, "loading", false);
@FXML
private VersionSettingsPage versionSettings;
@ -65,8 +68,6 @@ public final class VersionPage extends StackPane implements DecoratorPage {
@FXML
private JFXButton btnTestGame;
@FXML
private StackPane rootPane;
@FXML
private StackPane contentPane;
@FXML
private JFXTabPane tabPane;
@ -107,7 +108,6 @@ public final class VersionPage extends StackPane implements DecoratorPage {
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
FXUtils.installFastTooltip(btnTestGame, i18n("version.launch.test"));
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
@ -120,9 +120,13 @@ public final class VersionPage extends StackPane implements DecoratorPage {
versionSettings.loadVersion(profile, id);
mod.setParentTab(tabPane);
modTab.setUserData(mod);
mod.loadVersion(profile, id);
installer.loadVersion(profile, id);
world.loadVersion(profile, id);
loading.set(true);
CompletableFuture.allOf(
mod.loadVersion(profile, id),
installer.loadVersion(profile, id),
world.loadVersion(profile, id))
.whenCompleteAsync((result, exception) -> loading.set(false), Platform::runLater);
}
private void onNavigated(Navigator.NavigationEvent event) {
@ -163,7 +167,6 @@ public final class VersionPage extends StackPane implements DecoratorPage {
return title.get();
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
@ -171,4 +174,12 @@ public final class VersionPage extends StackPane implements DecoratorPage {
public void setTitle(String title) {
this.title.set(title);
}
public boolean isLoading() {
return loading.get();
}
public ReadOnlyBooleanProperty loadingProperty() {
return loading.getReadOnlyProperty();
}
}

View File

@ -0,0 +1,140 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXListView;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.Control;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class VersionRootPage extends Control implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final JFXListView<String> listView = new JFXListView<>();
private final VersionPage versionPage = new VersionPage();
private Profile profile;
{
Profiles.registerVersionsListener(this::loadVersions);
listView.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
loadVersion(newValue, profile);
});
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
List<String> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(Version::getId)
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
this.profile = profile;
loading.set(false);
listView.getItems().setAll(children);
}
});
}
public void loadVersion(String version, Profile profile) {
listView.getSelectionModel().select(version);
versionPage.load(version, profile);
}
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public static class Skin extends SkinBase<VersionRootPage> {
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
protected Skin(VersionRootPage control) {
super(control);
control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
// the root page, with the sidebar in left, navigator in center.
BorderPane root = new BorderPane();
root.getStyleClass().add("gray-background");
{
BorderPane leftRootPane = new BorderPane();
FXUtils.setLimitWidth(leftRootPane, 200);
StackPane drawerContainer = new StackPane();
drawerContainer.getChildren().setAll(control.listView);
leftRootPane.setCenter(drawerContainer);
Rectangle separator = new Rectangle();
separator.heightProperty().bind(root.heightProperty());
separator.setWidth(1);
separator.setFill(Color.GRAY);
leftRootPane.setRight(separator);
root.setLeft(leftRootPane);
}
root.setCenter(control.versionPage);
control.title.bind(control.versionPage.titleProperty());
spinnerPane.loadingProperty().bind(control.versionPage.loadingProperty());
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
}
}
}

View File

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

View File

@ -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<WorldListItem> {
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() {

View File

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

View File

@ -23,7 +23,7 @@ import javafx.beans.property.SimpleBooleanProperty;
public interface Refreshable {
void refresh();
default BooleanProperty canRefreshProperty() {
default BooleanProperty refreshableProperty() {
return new SimpleBooleanProperty(false);
}
}

View File

@ -5,10 +5,11 @@
<?import com.jfoenix.controls.*?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<?import org.jackhuang.hmcl.ui.animation.TransitionPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<StackPane fx:id="addServerContainer">
<TransitionPane fx:id="root">
<JFXDialogLayout fx:id="addServerPane">
<heading>
<Label text="%account.injector.add" />
@ -51,5 +52,5 @@
<JFXButton onMouseClicked="#onAddFinish" text="%wizard.finish" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
</StackPane>
</TransitionPane>
</fx:root>

View File

@ -3,6 +3,7 @@
<?import com.jfoenix.controls.*?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.animation.TransitionPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="BorderPane"
@ -13,7 +14,7 @@
</StackPane>
</top>
<center>
<StackPane fx:id="root">
<TransitionPane fx:id="root">
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<VBox fx:id="centrePane">
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
@ -30,6 +31,6 @@
<StackPane fx:id="emptyPane" styleClass="notice-pane">
<Label onMouseClicked="#onBack" text="%download.failed.empty" />
</StackPane>
</StackPane>
</TransitionPane>
</center>
</fx:root>

View File

@ -10,9 +10,7 @@
<?import org.jackhuang.hmcl.ui.versions.WorldListPage?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="rootPane"
type="StackPane">
<JFXRippler />
<StackPane fx:id="contentPane">
<JFXTabPane fx:id="tabPane">
<Tab text="%settings">

View File

@ -5,6 +5,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.animation.TransitionPane?>
<fx:root xmlns="http://javafx.com/javafx"
type="StackPane"
style="-fx-background-color: gray;"
@ -43,6 +44,6 @@
</rightItems>
</JFXToolbar>
<StackPane fx:id="root"/>
<TransitionPane fx:id="root"/>
</VBox>
</fx:root>