Make DatapackListPage multiple-selectable

This commit is contained in:
huanghongxun
2019-03-19 10:31:02 +08:00
parent 5953cc6411
commit 3d008dcab5
16 changed files with 396 additions and 445 deletions

View File

@@ -30,6 +30,7 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener; import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
@@ -161,6 +162,20 @@ public final class FXUtils {
}); });
} }
public static <K, T> void setupCellValueFactory(JFXTreeTableColumn<K, T> column, Function<K, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
});
}
public static Node wrapMargin(Node node, Insets insets) {
StackPane.setMargin(node, insets);
return new StackPane(node);
}
public static void setValidateWhileTextChanged(Node field, boolean validate) { public static void setValidateWhileTextChanged(Node field, boolean validate) {
if (field instanceof JFXTextField) { if (field instanceof JFXTextField) {
if (validate) { if (validate) {

View File

@@ -18,18 +18,11 @@
package org.jackhuang.hmcl.ui; package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control { public abstract class ListPage<T extends Node> extends ListPageBase<T> {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false); private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
public abstract void add(); public abstract void add();
@@ -42,30 +35,6 @@ public abstract class ListPage<T extends Node> extends Control {
return new ListPageSkin(this); return new ListPageSkin(this);
} }
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
public boolean isRefreshable() { public boolean isRefreshable() {
return refreshable.get(); return refreshable.get();
} }

View File

@@ -0,0 +1,55 @@
/*
* 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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
public class ListPageBase<T> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
}

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui; package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Group; import javafx.scene.Group;
import javafx.scene.Node; import javafx.scene.Node;
@@ -30,11 +29,8 @@ public final class SVG {
private SVG() { private SVG() {
} }
public static Node wrap(Node node) { public interface SVGIcon {
StackPane stackPane = new StackPane(); Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
} }
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) { private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {

View File

@@ -15,64 +15,42 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXScrollPane; import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager; import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
import javafx.scene.layout.*; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import static org.jackhuang.hmcl.ui.SVG.wrap; import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPageSkin extends SkinBase<WorldListPage> { public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>> extends SkinBase<T> {
public WorldListPageSkin(WorldListPage skinnable) { public ToolbarListPageSkin(T skinnable) {
super(skinnable); super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane(); SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane"); spinnerPane.getStyleClass().add("large-spinner-pane");
BorderPane contentPane = new BorderPane(); BorderPane root = new BorderPane();
{ {
HBox toolbar = new HBox(); HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second"); toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1); JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false); toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(initializeToolbar(skinnable));
JFXCheckBox chkShowAll = new JFXCheckBox(); root.setTop(toolbar);
chkShowAll.getStyleClass().add("jfx-tool-bar-checkbox");
chkShowAll.textFillProperty().bind(Theme.foregroundFillBinding());
chkShowAll.setText(i18n("world.show_all"));
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
toolbar.getChildren().add(chkShowAll);
JFXButton btnRefresh = new JFXButton();
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
btnRefresh.setText(i18n("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnAdd = new JFXButton();
btnAdd.getStyleClass().add("jfx-tool-bar-button");
btnAdd.textFillProperty().bind(Theme.foregroundFillBinding());
btnAdd.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAdd.setText(i18n("world.add"));
btnAdd.setOnMouseClicked(e -> skinnable.add());
toolbar.getChildren().add(btnAdd);
contentPane.setTop(toolbar);
} }
{ {
@@ -88,12 +66,31 @@ public class WorldListPageSkin extends SkinBase<WorldListPage> {
scrollPane.setContent(content); scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane); JFXScrollPane.smoothScrolling(scrollPane);
contentPane.setCenter(scrollPane); root.setCenter(scrollPane);
} }
spinnerPane.loadingProperty().bind(skinnable.loadingProperty()); spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(contentPane); spinnerPane.setContent(root);
getChildren().setAll(spinnerPane); getChildren().setAll(spinnerPane);
} }
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
protected abstract List<Node> initializeToolbar(T skinnable);
} }

View File

@@ -26,11 +26,12 @@ import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell; import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.StackPane;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.StringConverter; import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> { public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
private final StackPane pane;
private final CheckBox checkBox; private final CheckBox checkBox;
private boolean showLabel; private boolean showLabel;
private ObservableValue<Boolean> booleanProperty; private ObservableValue<Boolean> booleanProperty;
@@ -49,6 +50,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
final StringConverter<T> converter) { final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell"); this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox(); this.checkBox = new JFXCheckBox();
this.pane = new StackPane(checkBox);
this.pane.setAlignment(Pos.CENTER);
setGraphic(null); setGraphic(null);
setSelectedStateCallback(getSelectedProperty); setSelectedStateCallback(getSelectedProperty);
setConverter(converter); setConverter(converter);
@@ -101,7 +104,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
if (showLabel) { if (showLabel) {
setText(c.toString(item)); setText(c.toString(item));
} }
setGraphic(checkBox);
setGraphic(pane);
if (booleanProperty instanceof BooleanProperty) { if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty); checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);

View File

@@ -1,60 +0,0 @@
/*
* 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.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListItem extends BorderPane {
public DatapackListItem(Datapack.Pack info, Consumer<DatapackListItem> deleteCallback) {
JFXCheckBox chkEnabled = new JFXCheckBox();
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
setLeft(chkEnabled);
TwoLineListItem modItem = new TwoLineListItem();
BorderPane.setAlignment(modItem, Pos.CENTER);
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getId());
modItem.setSubtitle(info.getDescription());
chkEnabled.selectedProperty().bindBidirectional(info.activeProperty());
}
}

View File

@@ -19,11 +19,15 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack; import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPage; import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
@@ -32,16 +36,19 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.List;
import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage { public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(); private final StringProperty title = new SimpleStringProperty();
private final Path worldDir; private final Path worldDir;
private final Datapack datapack; private final Datapack datapack;
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) { public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir; this.worldDir = worldDir;
@@ -50,14 +57,7 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack = new Datapack(worldDir.resolve("datapacks")); datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir(); datapack.loadFromDir();
setItems(MappedObservableList.create(datapack.getInfo(), setItems(items = MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
})));
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), mods -> { FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> { mods.forEach(it -> {
@@ -69,7 +69,19 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e); Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
} }
}); });
}, datapack::loadFromDir); }, this::refresh);
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.of(datapack::loadFromDir)
.with(Task.of(() -> setLoading(false)))
.start();
} }
@Override @Override
@@ -77,7 +89,6 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
return title; return title;
} }
@Override
public void add() { public void add() {
FileChooser chooser = new FileChooser(); FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack")); chooser.setTitle(i18n("datapack.choose_datapack"));
@@ -97,4 +108,32 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack.loadFromDir(); datapack.loadFromDir();
} }
void removeSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
Logging.LOG.warning("Failed to delete datapack " + pack);
}
});
}
void enableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(true));
}
void disableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(false));
}
} }

View File

@@ -0,0 +1,126 @@
/*
* 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.versions;
import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class DatapackListPageSkin extends SkinBase<DatapackListPage> {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
JFXTreeTableView<DatapackInfoObject> tableView = new JFXTreeTableView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG::plus, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().addAll("no-header");
tableView.setShowRoot(false);
tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
JFXTreeTableColumn<DatapackInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(activeColumn, DatapackInfoObject::activeProperty);
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
activeColumn.setEditable(true);
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<DatapackInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, DatapackInfoObject::nodeProperty);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
getChildren().setAll(root);
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
private final BooleanProperty active;
private final Datapack.Pack packInfo;
private final ObjectProperty<Node> node;
DatapackInfoObject(Datapack.Pack packInfo) {
this.packInfo = packInfo;
this.active = packInfo.activeProperty();
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(packInfo.getId(), StringUtils.parseColorEscapes(packInfo.getDescription())),
new Insets(8, 0, 8, 0)));
}
BooleanProperty activeProperty() {
return active;
}
ObjectProperty<Node> nodeProperty() {
return node;
}
Datapack.Pack getPackInfo() {
return packInfo;
}
}
}

View File

@@ -18,10 +18,9 @@
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.ReadOnlyStringProperty;
import javafx.collections.FXCollections; import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.control.Control; import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.event.EventBus; import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent; import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
@@ -29,14 +28,14 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider; import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -45,17 +44,15 @@ import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameList extends Control implements DecoratorPage { public class GameList extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage")); private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage"));
private final BooleanProperty loading = new SimpleBooleanProperty(true);
private final ListProperty<GameListItem> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup; private ToggleGroup toggleGroup;
public GameList() { public GameList() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository()) if (event.getSource() == Profiles.getSelectedProfile().getRepository())
runInFX(() -> loading.set(true)); runInFX(() -> setLoading(true));
}); });
Profiles.registerVersionsListener(this::loadVersions); Profiles.registerVersionsListener(this::loadVersions);
@@ -74,8 +71,8 @@ public class GameList extends Control implements DecoratorPage {
.collect(Collectors.toList()); .collect(Collectors.toList());
runInFX(() -> { runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) { if (profile == Profiles.getSelectedProfile()) {
loading.set(false); setLoading(false);
items.setAll(children); itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection); children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
@@ -97,8 +94,8 @@ public class GameList extends Control implements DecoratorPage {
} }
@Override @Override
protected Skin<?> createDefaultSkin() { protected GameListSkin createDefaultSkin() {
return new GameListSkin(this); return new GameListSkin();
} }
void addNewGame() { void addNewGame() {
@@ -128,11 +125,20 @@ public class GameList extends Control implements DecoratorPage {
return title.getReadOnlyProperty(); return title.getReadOnlyProperty();
} }
public BooleanProperty loadingProperty() { private class GameListSkin extends ToolbarListPageSkin<GameList> {
return loading;
}
public ListProperty<GameListItem> itemsProperty() { public GameListSkin() {
return items; super(GameList.this);
}
@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)
);
}
} }
} }

View File

@@ -1,116 +0,0 @@
/*
* 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.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.i18n.I18n;
public class GameListSkin extends SkinBase<GameList> {
private static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public GameListSkin(GameList skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
JFXButton btnAddNewGame = new JFXButton();
btnAddNewGame.getStyleClass().add("jfx-tool-bar-button");
btnAddNewGame.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddNewGame.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddNewGame.setText(I18n.i18n("install.new_game"));
btnAddNewGame.setOnMouseClicked(e -> skinnable.addNewGame());
toolbar.getChildren().add(btnAddNewGame);
JFXButton btnImportModpack = new JFXButton();
btnImportModpack.getStyleClass().add("jfx-tool-bar-button");
btnImportModpack.textFillProperty().bind(Theme.foregroundFillBinding());
btnImportModpack.setGraphic(wrap(SVG.importIcon(Theme.foregroundFillBinding(), -1, -1)));
btnImportModpack.setText(I18n.i18n("install.modpack"));
btnImportModpack.setOnMouseClicked(e -> skinnable.importModpack());
toolbar.getChildren().add(btnImportModpack);
JFXButton btnRefresh = new JFXButton();
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
btnRefresh.setText(I18n.i18n("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnModify = new JFXButton();
btnModify.getStyleClass().add("jfx-tool-bar-button");
btnModify.textFillProperty().bind(Theme.foregroundFillBinding());
btnModify.setGraphic(wrap(SVG.gear(Theme.foregroundFillBinding(), -1, -1)));
btnModify.setText(I18n.i18n("settings.type.global.manage"));
btnModify.setOnMouseClicked(e -> skinnable.modifyGlobalGameSettings());
toolbar.getChildren().add(btnModify);
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.loadingProperty().bind(skinnable.loadingProperty());
center.getStyleClass().add("large-spinner-pane");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
VBox gameList = new VBox();
gameList.maxWidthProperty().bind(scrollPane.widthProperty());
gameList.setSpacing(10);
gameList.setStyle("-fx-padding: 10 10 10 10;");
Bindings.bindContent(gameList.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(gameList);
JFXScrollPane.smoothScrolling(scrollPane);
center.setContent(scrollPane);
root.setCenter(center);
}
getChildren().setAll(root);
}
}

View File

@@ -19,12 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTabPane; import com.jfoenix.controls.JFXTabPane;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.control.Control;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
@@ -36,6 +32,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
@@ -50,9 +47,7 @@ import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends Control { public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> {
private final ListProperty<ModListPageSkin.ModInfoObject> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private JFXTabPane parentTab; private JFXTabPane parentTab;
@@ -143,7 +138,7 @@ public final class ModListPage extends Control {
this.parentTab = parentTab; this.parentTab = parentTab;
} }
public void removeSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) { public void removeSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
try { try {
modManager.removeMods(selectedItems.stream() modManager.removeMods(selectedItems.stream()
.map(TreeItem::getValue) .map(TreeItem::getValue)
@@ -155,44 +150,20 @@ public final class ModListPage extends Control {
} }
} }
public void enableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) { public void enableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream() selectedItems.stream()
.map(TreeItem::getValue) .map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo) .map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true)); .forEach(info -> info.setActive(true));
} }
public void disableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) { public void disableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream() selectedItems.stream()
.map(TreeItem::getValue) .map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo) .map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(false)); .forEach(info -> info.setActive(false));
} }
public ObservableList<ModListPageSkin.ModInfoObject> getItems() {
return items.get();
}
public void setItems(ObservableList<ModListPageSkin.ModInfoObject> items) {
this.items.set(items);
}
public ListProperty<ModListPageSkin.ModInfoObject> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
public boolean isModded() { public boolean isModded() {
return modded.get(); return modded.get();
} }

View File

@@ -17,36 +17,38 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*; import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject; import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager; import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.text.TextAlignment;
import javafx.util.Callback;
import org.jackhuang.hmcl.mod.ModInfo; import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell; import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Function; import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.SVG.wrap; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModListPageSkin extends SkinBase<ModListPage> { class ModListPageSkin extends SkinBase<ModListPage> {
public ModListPageSkin(ModListPage skinnable) { ModListPageSkin(ModListPage skinnable) {
super(skinnable); super(skinnable);
StackPane pane = new StackPane(); StackPane pane = new StackPane();
@@ -61,46 +63,14 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
JFXDepthManager.setDepth(toolbar, 1); JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false); toolbar.setPickOnBounds(false);
JFXButton btnRefresh = new JFXButton(); toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
btnRefresh.getStyleClass().add("jfx-tool-bar-button"); toolbar.getChildren().add(createToolbarButton(i18n("mods.add"), SVG::plus, skinnable::add));
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding()); toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1))); skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
btnRefresh.setText(i18n("button.refresh")); toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
btnRefresh.setOnMouseClicked(e -> skinnable.refresh()); skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(btnRefresh); toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
JFXButton btnAddMod = new JFXButton();
btnAddMod.getStyleClass().add("jfx-tool-bar-button");
btnAddMod.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddMod.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddMod.setText(i18n("mods.add"));
btnAddMod.setOnMouseClicked(e -> skinnable.add());
toolbar.getChildren().add(btnAddMod);
JFXButton btnRemove = new JFXButton();
btnRemove.getStyleClass().add("jfx-tool-bar-button");
btnRemove.textFillProperty().bind(Theme.foregroundFillBinding());
btnRemove.setGraphic(wrap(SVG.delete(Theme.foregroundFillBinding(), -1, -1)));
btnRemove.setText(i18n("mods.remove"));
btnRemove.setOnMouseClicked(e -> skinnable.removeSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnRemove);
JFXButton btnEnable = new JFXButton();
btnEnable.getStyleClass().add("jfx-tool-bar-button");
btnEnable.textFillProperty().bind(Theme.foregroundFillBinding());
btnEnable.setGraphic(wrap(SVG.check(Theme.foregroundFillBinding(), -1, -1)));
btnEnable.setText(i18n("mods.enable"));
btnEnable.setOnMouseClicked(e -> skinnable.enableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnEnable);
JFXButton btnDisable = new JFXButton();
btnDisable.getStyleClass().add("jfx-tool-bar-button");
btnDisable.textFillProperty().bind(Theme.foregroundFillBinding());
btnDisable.setGraphic(wrap(SVG.close(Theme.foregroundFillBinding(), -1, -1)));
btnDisable.setText(i18n("mods.disable"));
btnDisable.setOnMouseClicked(e -> skinnable.disableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnDisable);
root.setTop(toolbar); root.setTop(toolbar);
} }
@@ -109,6 +79,7 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
center.getStyleClass().add("large-spinner-pane"); center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty()); center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().add("no-header");
tableView.setShowRoot(false); tableView.setShowRoot(false);
tableView.setEditable(true); tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
@@ -121,18 +92,10 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
activeColumn.setMaxWidth(40); activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40); activeColumn.setMinWidth(40);
JFXTreeTableColumn<ModInfoObject, String> fileNameColumn = new JFXTreeTableColumn<>(); JFXTreeTableColumn<ModInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
fileNameColumn.setText(i18n("archive.name")); setupCellValueFactory(detailColumn, ModInfoObject::nodeProperty);
setupCellValueFactory(fileNameColumn, ModInfoObject::fileNameProperty);
fileNameColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.8));
JFXTreeTableColumn<ModInfoObject, String> versionColumn = new JFXTreeTableColumn<>(); tableView.getColumns().setAll(activeColumn, detailColumn);
versionColumn.setText(i18n("archive.version"));
versionColumn.setPrefWidth(100);
setupCellValueFactory(versionColumn, ModInfoObject::versionProperty);
versionColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.2));
tableView.getColumns().setAll(activeColumn, fileNameColumn, versionColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY); tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView); center.setContent(tableView);
@@ -150,44 +113,33 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
getChildren().setAll(pane); getChildren().setAll(pane);
} }
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModInfoObject, T> column, Function<ModInfoObject, ObservableValue<T>> mapper) { static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
column.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<ModInfoObject, T>, ObservableValue<T>>() {
@Override
public ObservableValue<T> call(TreeTableColumn.CellDataFeatures<ModInfoObject, T> param) {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
}
});
}
public static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
private final BooleanProperty active; private final BooleanProperty active;
private final StringProperty fileName;
private final StringProperty version;
private final ModInfo modInfo; private final ModInfo modInfo;
private final ObjectProperty<Node> node;
public ModInfoObject(ModInfo modInfo) { ModInfoObject(ModInfo modInfo) {
this.modInfo = modInfo; this.modInfo = modInfo;
this.active = modInfo.activeProperty(); this.active = modInfo.activeProperty();
this.fileName = new SimpleStringProperty(modInfo.getFileName()); StringBuilder message = new StringBuilder(modInfo.getName());
this.version = new SimpleStringProperty(modInfo.getVersion()); if (isNotBlank(modInfo.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(modInfo.getVersion());
if (isNotBlank(modInfo.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(modInfo.getGameVersion());
if (isNotBlank(modInfo.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(modInfo.getAuthors());
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(modInfo.getFileName(), message.toString()), new Insets(8, 0, 8, 0)));
} }
public BooleanProperty activeProperty() { BooleanProperty activeProperty() {
return active; return active;
} }
public StringProperty fileNameProperty() { ObjectProperty<Node> nodeProperty() {
return fileName; return node;
} }
public StringProperty versionProperty() { ModInfo getModInfo() {
return version;
}
public ModInfo getModInfo() {
return modInfo; return modInfo;
} }
} }

View File

@@ -17,38 +17,32 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty; import javafx.scene.Node;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameVersion; import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File; import java.io.File;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPage extends Control { public class WorldListPage extends ListPageBase<WorldListItem> {
private final ListProperty<WorldListItem> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir; private Path savesDir;
@@ -71,8 +65,8 @@ public class WorldListPage extends Control {
} }
@Override @Override
protected Skin<?> createDefaultSkin() { protected ToolbarListPageSkin createDefaultSkin() {
return new WorldListPageSkin(this); return new WorldListPageSkin();
} }
public void loadVersion(Profile profile, String id) { public void loadVersion(Profile profile, String id) {
@@ -145,27 +139,23 @@ public class WorldListPage extends Control {
this.showAll.set(showAll); this.showAll.set(showAll);
} }
public ObservableList<WorldListItem> getItems() { private class WorldListPageSkin extends ToolbarListPageSkin<WorldListPage> {
return items.get();
}
public ListProperty<WorldListItem> itemsProperty() { WorldListPageSkin() {
return items; super(WorldListPage.this);
} }
public void setItems(ObservableList<WorldListItem> items) { @Override
this.items.set(items); protected List<Node> initializeToolbar(WorldListPage skinnable) {
} JFXCheckBox chkShowAll = new JFXCheckBox();
chkShowAll.getStyleClass().add("jfx-tool-bar-checkbox");
chkShowAll.textFillProperty().bind(Theme.foregroundFillBinding());
chkShowAll.setText(i18n("world.show_all"));
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
public boolean isLoading() { return Arrays.asList(chkShowAll,
return loading.get(); createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
} createToolbarButton(i18n("world.add"), SVG::plus, skinnable::add));
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
} }
} }

View File

@@ -1025,6 +1025,12 @@
* * * *
*******************************************************************************/ *******************************************************************************/
.no-header .column-header-background {
-fx-max-height: 0;
-fx-pref-height: 0;
-fx-min-height: 0;
}
.tree-table-view { .tree-table-view {
-fx-tree-table-color: rgba(82, 100, 174, 0.4); -fx-tree-table-color: rgba(82, 100, 174, 0.4);
-fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6); -fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6);
@@ -1075,7 +1081,7 @@
.tree-table-view .tree-table-cell { .tree-table-view .tree-table-cell {
-fx-border-width: 0 0 0 0; -fx-border-width: 0 0 0 0;
-fx-padding: 8 0 8 0; -fx-padding: 0 0 0 0;
/*-fx-alignment: top-center;*/ /*-fx-alignment: top-center;*/
} }

View File

@@ -24,7 +24,8 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -128,7 +129,7 @@ public class Datapack {
isMultiple = false; isMultiple = false;
try { try {
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class); PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this)); Platform.runLater(() -> info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this)));
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + path, e); Logging.LOG.log(Level.WARNING, "Failed to read datapack " + path, e);
} }
@@ -192,7 +193,7 @@ public class Datapack {
} }
} }
this.info.setAll(info); Platform.runLater(() -> this.info.setAll(info));
} }
public static class Pack { public static class Pack {