Merge branch 'gui' into javafx

This commit is contained in:
huanghongxun
2020-03-02 16:21:22 +08:00
49 changed files with 1945 additions and 764 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.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<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

@@ -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 <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c)));
public static <T> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
value.addListener(listener);
return listener;
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
@@ -109,9 +112,9 @@ public final class FXUtils {
onChange(value, consumer);
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> 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();

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

@@ -178,4 +178,8 @@ public final class SVG {
public static Node arrowRight(ObjectBinding<? extends Paint> 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<? extends Paint> 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);
}
}

View File

@@ -44,12 +44,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
BorderPane root = new BorderPane();
{
List<Node> 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);
}

View File

@@ -15,23 +15,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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 com.jfoenix.controls.JFXButton;

View File

@@ -70,9 +70,9 @@ public class AccountAdvancedListItem extends AdvancedListItem {
ObservableList<Account> 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()));
});

View File

@@ -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<AccountListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
@@ -51,7 +52,7 @@ public class AccountList extends ListPage<AccountListItem> implements DecoratorP
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}

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,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<AuthlibInjectorServerItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.injector.manage.title")));
private final ObservableList<AuthlibInjectorServerItem> serverItems;
@@ -50,16 +49,8 @@ public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerIt
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
public String getTitle() {
return title.get();
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public void setTitle(String title) {
this.title.set(title);
public ReadOnlyObjectWrapper<State> stateProperty() {
return state;
}
}

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

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

View File

@@ -0,0 +1,195 @@
/**
* 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.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<ScrollBar> {
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;
}
}

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,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<PageCloseEvent> 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<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

@@ -0,0 +1,504 @@
/*
* 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.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<Tab> tabs = FXCollections.observableArrayList();
public ObservableList<Tab> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabHeaderSelectionModel(this));
public SingleSelectionModel<Tab> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<Tab> selectionModel) {
this.selectionModel.set(selectionModel);
}
static class TabHeaderSelectionModel extends SingleSelectionModel<Tab> {
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<Tab> 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<Tab> 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<Tab> 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<TabHeader> {
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<Node> 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);
}
}
}

View File

@@ -35,7 +35,7 @@ public class Decorator extends Control {
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
private final StringProperty title = new SimpleStringProperty();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final StringProperty drawerTitle = new SimpleStringProperty();
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> 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<DecoratorPage.State> 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() {

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

View File

@@ -0,0 +1,88 @@
/*
* 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.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());
}
}
}

View File

@@ -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<State> 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;
}
}
}

View File

@@ -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<Decorator> {
@@ -45,6 +50,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
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<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);
// 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);
@@ -149,7 +131,17 @@ public class DecoratorSkin extends SkinBase<Decorator> {
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<Decorator> {
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<Decorator> {
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<Decorator> {
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;
}

View File

@@ -0,0 +1,68 @@
/*
* 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.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> 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<State> stateProperty() {
return state.getReadOnlyProperty();
}
}

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,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<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;
@@ -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> 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<Node> versions = FXCollections.observableArrayList();
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private final ObservableList<Node> 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<State> stateProperty() {
return state;
}
public String getCurrentGame() {
@@ -257,7 +277,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.latestVersion.set(latestVersion);
}
public ObservableList<Node> getVersions() {
return versions;
public void initVersions(Profile profile, List<Version> versions) {
FXUtils.checkFxUserThread();
this.profile = profile;
this.versions.setAll(versions);
}
}

View File

@@ -0,0 +1,269 @@
/*
* 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.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<Version> 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<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();
{
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();
});
}
}

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;
@@ -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> 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<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override

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

@@ -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<ProfileListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(i18n("profile.manage"));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("profile.manage")));
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
private ObjectProperty<Profile> selectedProfile;
@@ -52,7 +52,7 @@ public class ProfileList extends ListPage<ProfileListItem> implements DecoratorP
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}

View File

@@ -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> 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<State> stateProperty() {
return state.getReadOnlyProperty();
}
public String getLocation() {

View File

@@ -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<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final Path worldDir;
private final Datapack datapack;
@@ -52,7 +52,7 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
title.set(i18n("datapack.title", worldName));
state.set(State.fromTitle(i18n("datapack.title", worldName)));
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
@@ -86,8 +86,8 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
}
@Override
public StringProperty titleProperty() {
return title;
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public void add() {

View File

@@ -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<GameListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage"));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
private ToggleGroup toggleGroup;
@@ -123,24 +123,29 @@ public class GameList extends ListPageBase<GameListItem> implements DecoratorPag
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
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<Node> 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();
}
}
}

View File

@@ -91,7 +91,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
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);

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

@@ -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<ModListPageSkin.ModInfoObject> {
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<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)
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<ModListPageSkin.ModInfoObjec
}).start();
}
public void setParentTab(JFXTabPane parentTab) {
this.parentTab = parentTab;
}
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()

View File

@@ -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> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final JFXListView<String> 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<TabHeader.Tab> 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<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);
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<State> stateProperty() {
return state.getReadOnlyProperty();
}
public static class Skin extends SkinBase<VersionPage> {
/**
* 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);
}
}
}

View File

@@ -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> 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<State> stateProperty() {
return state.getReadOnlyProperty();
}
}

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

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

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>

View File

@@ -58,6 +58,20 @@ public final class Lang {
return Collections.unmodifiableList(Arrays.asList(elements));
}
public static <T extends Comparable<T>> 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();