alt: version page look

This commit is contained in:
huanghongxun 2020-03-02 14:27:14 +08:00
parent fa13b1fc0d
commit 11d47bf9b1
10 changed files with 763 additions and 287 deletions

View File

@ -34,7 +34,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.VersionRootPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@ -50,7 +50,7 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static VersionRootPage versionPage = null;
private static VersionPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static DecoratorController decorator;
@ -64,9 +64,9 @@ public final class Controllers {
}
// FXThread
public static VersionRootPage getVersionPage() {
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionRootPage();
versionPage = new VersionPage();
return versionPage;
}

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

@ -131,6 +131,10 @@ public class Navigator extends TransitionPane {
return stack.size() > 1;
}
public int size() {
return stack.size();
}
public void setContent(Node content, AnimationProducer animationProducer) {
super.setContent(content, animationProducer);

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

@ -265,17 +265,22 @@ public class DecoratorController {
decorator.canRefreshProperty().set(false);
}
decorator.canCloseProperty().set(navigator.size() > 2);
if (to instanceof DecoratorPage) {
decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable());
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
} else {
decorator.showCloseAsHomeProperty().set(true);
}
// 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));
}
decorator.canCloseProperty().set(navigator.canGoBack());
if (to instanceof Region) {
Region region = (Region) to;
// Let root pane fix window size.

View File

@ -213,22 +213,23 @@ public class DecoratorSkin extends SkinBase<Decorator> {
}
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().set(canRefresh);
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

@ -17,7 +17,6 @@
*/
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;
@ -33,6 +32,7 @@ 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;
@ -53,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 -> {
@ -101,10 +102,12 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}).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()));
});
}));
else
getProperties().remove(ModListPage.class);
}, Platform::runLater);
}
@ -141,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,114 +18,100 @@
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.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
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 {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", null);
private final ReadOnlyBooleanWrapper loading = new ReadOnlyBooleanWrapper(this, "loading", false);
@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 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))
);
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"));
listView.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
loadVersion(newValue, profile);
});
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);
versionSettings.loadVersion(profile, id);
mod.setParentTab(tabPane);
modTab.setUserData(mod);
versionSettingsPage.loadVersion(profile, version);
loading.set(true);
CompletableFuture.allOf(
mod.loadVersion(profile, id),
installer.loadVersion(profile, id),
world.loadVersion(profile, id))
modListPage.loadVersion(profile, version),
installerListPage.loadVersion(profile, version),
worldListPage.loadVersion(profile, version))
.whenCompleteAsync((result, exception) -> loading.set(false), Platform::runLater);
}
@ -141,45 +127,164 @@ public final class VersionPage extends StackPane {
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);
}
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
}
public void setTitle(String title) {
this.title.set(title);
private void clearJunkFiles() {
Versions.cleanVersion(profile, version);
}
public boolean isLoading() {
return loading.get();
private void testGame() {
Versions.testGame(profile, version);
}
public ReadOnlyBooleanProperty loadingProperty() {
return loading.getReadOnlyProperty();
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
}
@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

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

View File

@ -32,12 +32,6 @@
-fx-arc-height: 5px;
}
.list-view,
.scroll-pane,
.scroll-pane > .viewport {
-fx-background-color: transparent;
}
.disabled Label {
-fx-text-fill: rgba(0, 0, 0, 0.5);
}
@ -1044,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;