mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-15 14:56:05 -04:00
Use TableView in ModListPage to allow multiple selection
This commit is contained in:
parent
b3721e29c6
commit
c2d609cdb4
@ -37,6 +37,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
|||||||
super(skinnable);
|
super(skinnable);
|
||||||
|
|
||||||
SpinnerPane spinnerPane = new SpinnerPane();
|
SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
|
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||||
Pane placeholder = new Pane();
|
Pane placeholder = new Pane();
|
||||||
|
|
||||||
StackPane contentPane = new StackPane();
|
StackPane contentPane = new StackPane();
|
||||||
|
@ -162,4 +162,8 @@ public final class SVG {
|
|||||||
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
|
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||||
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
|
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||||
|
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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.construct;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXCheckBox;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.TreeTableCell;
|
||||||
|
import javafx.util.Callback;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
|
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
|
||||||
|
|
||||||
|
private final CheckBox checkBox;
|
||||||
|
private boolean showLabel;
|
||||||
|
private ObservableValue<Boolean> booleanProperty;
|
||||||
|
|
||||||
|
public JFXCheckBoxTreeTableCell() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JFXCheckBoxTreeTableCell(
|
||||||
|
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
|
||||||
|
this(getSelectedProperty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JFXCheckBoxTreeTableCell(
|
||||||
|
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
|
||||||
|
final StringConverter<T> converter) {
|
||||||
|
this.getStyleClass().add("check-box-tree-table-cell");
|
||||||
|
this.checkBox = new JFXCheckBox();
|
||||||
|
setGraphic(null);
|
||||||
|
setSelectedStateCallback(getSelectedProperty);
|
||||||
|
setConverter(converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectProperty<StringConverter<T>> converter =
|
||||||
|
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
|
||||||
|
protected void invalidated() {
|
||||||
|
updateShowLabel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public final ObjectProperty<StringConverter<T>> converterProperty() {
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
public final void setConverter(StringConverter<T> value) {
|
||||||
|
converterProperty().set(value);
|
||||||
|
}
|
||||||
|
public final StringConverter<T> getConverter() {
|
||||||
|
return converterProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
|
||||||
|
selectedStateCallback =
|
||||||
|
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
|
||||||
|
this, "selectedStateCallback");
|
||||||
|
|
||||||
|
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
|
||||||
|
return selectedStateCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
|
||||||
|
selectedStateCallbackProperty().set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
|
||||||
|
return selectedStateCallbackProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override public void updateItem(T item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
StringConverter<T> c = getConverter();
|
||||||
|
|
||||||
|
if (showLabel) {
|
||||||
|
setText(c.toString(item));
|
||||||
|
}
|
||||||
|
setGraphic(checkBox);
|
||||||
|
|
||||||
|
if (booleanProperty instanceof BooleanProperty) {
|
||||||
|
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
|
||||||
|
}
|
||||||
|
ObservableValue<?> obsValue = getSelectedProperty();
|
||||||
|
if (obsValue instanceof BooleanProperty) {
|
||||||
|
booleanProperty = (ObservableValue<Boolean>) obsValue;
|
||||||
|
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBox.disableProperty().bind(Bindings.not(
|
||||||
|
getTreeTableView().editableProperty().and(
|
||||||
|
getTableColumn().editableProperty()).and(
|
||||||
|
editableProperty())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateShowLabel() {
|
||||||
|
this.showLabel = converter != null;
|
||||||
|
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableValue<?> getSelectedProperty() {
|
||||||
|
return getSelectedStateCallback() != null ?
|
||||||
|
getSelectedStateCallback().call(getIndex()) :
|
||||||
|
getTableColumn().getCellObservableValue(getIndex());
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.versions;
|
|||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXScrollPane;
|
import com.jfoenix.controls.JFXScrollPane;
|
||||||
import com.jfoenix.controls.JFXSpinner;
|
|
||||||
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;
|
||||||
@ -31,8 +30,8 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
|
|
||||||
public class GameListSkin extends SkinBase<GameList> {
|
public class GameListSkin extends SkinBase<GameList> {
|
||||||
@ -91,10 +90,9 @@ public class GameListSkin extends SkinBase<GameList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
StackPane center = new StackPane();
|
SpinnerPane center = new SpinnerPane();
|
||||||
|
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||||
JFXSpinner spinner = new JFXSpinner();
|
center.getStyleClass().add("large-spinner-pane");
|
||||||
spinner.getStyleClass().setAll("first-spinner");
|
|
||||||
|
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
scrollPane.setFitToWidth(true);
|
scrollPane.setFitToWidth(true);
|
||||||
@ -109,9 +107,7 @@ public class GameListSkin extends SkinBase<GameList> {
|
|||||||
scrollPane.setContent(gameList);
|
scrollPane.setContent(gameList);
|
||||||
JFXScrollPane.smoothScrolling(scrollPane);
|
JFXScrollPane.smoothScrolling(scrollPane);
|
||||||
|
|
||||||
FXUtils.onChangeAndOperate(skinnable.loadingProperty(),
|
center.setContent(scrollPane);
|
||||||
loading -> center.getChildren().setAll(loading ? spinner : scrollPane));
|
|
||||||
|
|
||||||
root.setCenter(center);
|
root.setCenter(center);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,73 +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.concurrency.JFXUtilities;
|
|
||||||
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.ModInfo;
|
|
||||||
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 org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
public final class ModItem extends BorderPane {
|
|
||||||
|
|
||||||
public ModItem(ModInfo info, Consumer<ModItem> 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();
|
|
||||||
JFXUtilities.runInFX(() -> {
|
|
||||||
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
|
|
||||||
});
|
|
||||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
|
||||||
btnRemove.getStyleClass().add("toggle-icon4");
|
|
||||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
|
||||||
btnRemove.setGraphic(SVG.delete(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.getFileName());
|
|
||||||
StringBuilder message = new StringBuilder(info.getName());
|
|
||||||
if (StringUtils.isNotBlank(info.getVersion()))
|
|
||||||
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
|
|
||||||
if (StringUtils.isNotBlank(info.getGameVersion()))
|
|
||||||
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
|
|
||||||
if (StringUtils.isNotBlank(info.getAuthors()))
|
|
||||||
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
|
|
||||||
modItem.setSubtitle(message.toString());
|
|
||||||
chkEnabled.setSelected(info.isActive());
|
|
||||||
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
|
|
||||||
info.activeProperty().set(newValue));
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,6 +19,15 @@ package org.jackhuang.hmcl.ui.versions;
|
|||||||
|
|
||||||
import com.jfoenix.concurrency.JFXUtilities;
|
import com.jfoenix.concurrency.JFXUtilities;
|
||||||
import com.jfoenix.controls.JFXTabPane;
|
import com.jfoenix.controls.JFXTabPane;
|
||||||
|
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;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.mod.ModInfo;
|
import org.jackhuang.hmcl.mod.ModInfo;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
@ -37,16 +46,18 @@ import java.util.Arrays;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class ModListPage extends ListPage<ModItem> {
|
public final class ModListPage extends Control {
|
||||||
|
private final ListProperty<ModListPageSkin.ModInfoObject> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||||
|
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
|
||||||
|
|
||||||
private JFXTabPane parentTab;
|
private JFXTabPane parentTab;
|
||||||
private ModManager modManager;
|
private ModManager modManager;
|
||||||
|
|
||||||
public ModListPage() {
|
public ModListPage() {
|
||||||
setRefreshable(true);
|
|
||||||
|
|
||||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||||
mods.forEach(it -> {
|
mods.forEach(it -> {
|
||||||
@ -61,6 +72,10 @@ public final class ModListPage extends ListPage<ModItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
return new ModListPageSkin(this);
|
||||||
|
}
|
||||||
|
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
loadMods(modManager);
|
loadMods(modManager);
|
||||||
}
|
}
|
||||||
@ -71,50 +86,22 @@ public final class ModListPage extends ListPage<ModItem> {
|
|||||||
|
|
||||||
public void loadMods(ModManager modManager) {
|
public void loadMods(ModManager modManager) {
|
||||||
this.modManager = modManager;
|
this.modManager = modManager;
|
||||||
Task.of(variables -> {
|
Task.ofResult("list", variables -> {
|
||||||
synchronized (ModListPage.this) {
|
synchronized (ModListPage.this) {
|
||||||
JFXUtilities.runInFX(() -> loadingProperty().set(true));
|
JFXUtilities.runInFX(() -> loadingProperty().set(true));
|
||||||
|
|
||||||
modManager.refreshMods();
|
modManager.refreshMods();
|
||||||
|
return new LinkedList<>(modManager.getMods());
|
||||||
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
|
|
||||||
// constructing UI elements.
|
|
||||||
// We must do this asynchronously.
|
|
||||||
LinkedList<ModItem> list = new LinkedList<>();
|
|
||||||
for (ModInfo modInfo : modManager.getMods()) {
|
|
||||||
ModItem item = new ModItem(modInfo, i -> {
|
|
||||||
try {
|
|
||||||
modManager.removeMods(modInfo);
|
|
||||||
} catch (IOException ignore) {
|
|
||||||
// Fail to remove mods if the game is running or the mod is absent.
|
|
||||||
}
|
|
||||||
loadMods(modManager);
|
|
||||||
});
|
|
||||||
modInfo.activeProperty().addListener((a, b, newValue) -> {
|
|
||||||
if (newValue)
|
|
||||||
item.getStyleClass().remove("disabled");
|
|
||||||
else
|
|
||||||
item.getStyleClass().add("disabled");
|
|
||||||
});
|
|
||||||
if (!modInfo.isActive())
|
|
||||||
item.getStyleClass().add("disabled");
|
|
||||||
|
|
||||||
list.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
variables.set("list", list);
|
|
||||||
}
|
}
|
||||||
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
}).finalizedResult(Schedulers.javafx(), (list, isDependentsSucceeded) -> {
|
||||||
loadingProperty().set(false);
|
loadingProperty().set(false);
|
||||||
if (isDependentsSucceeded)
|
if (isDependentsSucceeded)
|
||||||
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
|
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||||
if (newValue != null && newValue.getUserData() == ModListPage.this)
|
if (newValue != null && newValue.getUserData() == ModListPage.this)
|
||||||
itemsProperty().setAll(variables.<List<ModItem>>get("list"));
|
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList()));
|
||||||
});
|
});
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void add() {
|
public void add() {
|
||||||
FileChooser chooser = new FileChooser();
|
FileChooser chooser = new FileChooser();
|
||||||
chooser.setTitle(i18n("mods.choose_mod"));
|
chooser.setTitle(i18n("mods.choose_mod"));
|
||||||
@ -151,4 +138,54 @@ public final class ModListPage extends ListPage<ModItem> {
|
|||||||
public void setParentTab(JFXTabPane parentTab) {
|
public void setParentTab(JFXTabPane parentTab) {
|
||||||
this.parentTab = parentTab;
|
this.parentTab = parentTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||||
|
try {
|
||||||
|
modManager.removeMods(selectedItems.stream()
|
||||||
|
.map(TreeItem::getValue)
|
||||||
|
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||||
|
.toArray(ModInfo[]::new));
|
||||||
|
loadMods(modManager);
|
||||||
|
} catch (IOException ignore) {
|
||||||
|
// Fail to remove mods if the game is running or the mod is absent.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||||
|
selectedItems.stream()
|
||||||
|
.map(TreeItem::getValue)
|
||||||
|
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||||
|
.forEach(info -> info.setActive(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||||
|
selectedItems.stream()
|
||||||
|
.map(TreeItem::getValue)
|
||||||
|
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* 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.*;
|
||||||
|
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||||
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.util.Callback;
|
||||||
|
import org.jackhuang.hmcl.mod.ModInfo;
|
||||||
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
|
public class ModListPageSkin extends SkinBase<ModListPage> {
|
||||||
|
|
||||||
|
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 ModListPageSkin(ModListPage skinnable) {
|
||||||
|
super(skinnable);
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
JFXTreeTableView<ModInfoObject> tableView = new JFXTreeTableView<>();
|
||||||
|
|
||||||
|
{
|
||||||
|
HBox toolbar = new HBox();
|
||||||
|
toolbar.getStyleClass().setAll("jfx-tool-bar-second");
|
||||||
|
JFXDepthManager.setDepth(toolbar, 1);
|
||||||
|
toolbar.setPickOnBounds(false);
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SpinnerPane center = new SpinnerPane();
|
||||||
|
center.getStyleClass().add("large-spinner-pane");
|
||||||
|
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||||
|
|
||||||
|
tableView.setShowRoot(false);
|
||||||
|
tableView.setEditable(true);
|
||||||
|
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
|
||||||
|
|
||||||
|
JFXTreeTableColumn<ModInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
|
||||||
|
setupCellValueFactory(activeColumn, ModInfoObject::activeProperty);
|
||||||
|
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
|
||||||
|
activeColumn.setEditable(true);
|
||||||
|
activeColumn.setMaxWidth(40);
|
||||||
|
activeColumn.setMinWidth(40);
|
||||||
|
|
||||||
|
JFXTreeTableColumn<ModInfoObject, String> fileNameColumn = new JFXTreeTableColumn<>();
|
||||||
|
fileNameColumn.setText(i18n("archive.name"));
|
||||||
|
setupCellValueFactory(fileNameColumn, ModInfoObject::fileNameProperty);
|
||||||
|
fileNameColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.8));
|
||||||
|
|
||||||
|
JFXTreeTableColumn<ModInfoObject, String> versionColumn = new JFXTreeTableColumn<>();
|
||||||
|
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);
|
||||||
|
center.setContent(tableView);
|
||||||
|
root.setCenter(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren().setAll(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModInfoObject, T> column, Function<ModInfoObject, ObservableValue<T>> mapper) {
|
||||||
|
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 StringProperty fileName;
|
||||||
|
private final StringProperty version;
|
||||||
|
private final ModInfo modInfo;
|
||||||
|
|
||||||
|
public ModInfoObject(ModInfo modInfo) {
|
||||||
|
this.modInfo = modInfo;
|
||||||
|
this.active = modInfo.activeProperty();
|
||||||
|
this.fileName = new SimpleStringProperty(modInfo.getFileName());
|
||||||
|
this.version = new SimpleStringProperty(modInfo.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty activeProperty() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty fileNameProperty() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty versionProperty() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModInfo getModInfo() {
|
||||||
|
return modInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -876,6 +876,14 @@
|
|||||||
-fx-stroke-width: 3.0;
|
-fx-stroke-width: 3.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.large-spinner-pane .jfx-spinner {
|
||||||
|
-jfx-radius: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-spinner-pane .jfx-spinner {
|
||||||
|
-fx-stroke-width: 5.0;
|
||||||
|
}
|
||||||
|
|
||||||
.second-spinner {
|
.second-spinner {
|
||||||
-jfx-radius: 30;
|
-jfx-radius: 30;
|
||||||
}
|
}
|
||||||
@ -1028,7 +1036,7 @@
|
|||||||
|
|
||||||
.tree-table-view .column-header .label {
|
.tree-table-view .column-header .label {
|
||||||
-fx-text-fill: #949494;
|
-fx-text-fill: #949494;
|
||||||
-fx-padding: 16 0 16 0;
|
-fx-padding: 8 0 8 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
|
.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
|
||||||
@ -1046,8 +1054,8 @@
|
|||||||
|
|
||||||
.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: 16 0 16 0;
|
-fx-padding: 8 0 8 0;
|
||||||
-fx-alignment: top-center;
|
/*-fx-alignment: top-center;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-view .column-overlay {
|
.tree-table-view .column-overlay {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<fx:define>
|
<fx:define>
|
||||||
<Insets fx:id="insets" bottom="12" />
|
<Insets fx:id="insets" bottom="12" />
|
||||||
</fx:define>
|
</fx:define>
|
||||||
<SpinnerPane fx:id="spinnerPane">
|
<SpinnerPane fx:id="spinnerPane" styleClass="large-spinner-pane">
|
||||||
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
|
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
|
||||||
<HBox style="-fx-padding: 0 0 16 5;"><Label text="%modpack.task.install" /></HBox>
|
<HBox style="-fx-padding: 0 0 16 5;"><Label text="%modpack.task.install" /></HBox>
|
||||||
<ComponentList>
|
<ComponentList>
|
||||||
|
@ -66,6 +66,7 @@ account.username=Name
|
|||||||
|
|
||||||
archive.author=Authors
|
archive.author=Authors
|
||||||
archive.game_version=Game
|
archive.game_version=Game
|
||||||
|
archive.name=Name
|
||||||
archive.version=Version
|
archive.version=Version
|
||||||
|
|
||||||
assets.download=Download assets
|
assets.download=Download assets
|
||||||
@ -252,6 +253,9 @@ mods.add=Add mods
|
|||||||
mods.add.failed=Failed to add mods %s.
|
mods.add.failed=Failed to add mods %s.
|
||||||
mods.add.success=Successfully added mods %s.
|
mods.add.success=Successfully added mods %s.
|
||||||
mods.choose_mod=Choose your mods
|
mods.choose_mod=Choose your mods
|
||||||
|
mods.enable=Enable
|
||||||
|
mods.disable=Disable
|
||||||
|
mods.name=Name
|
||||||
mods.remove=Remove
|
mods.remove=Remove
|
||||||
|
|
||||||
datapack=Data packs
|
datapack=Data packs
|
||||||
|
@ -65,6 +65,7 @@ account.username=使用者名稱
|
|||||||
|
|
||||||
archive.author=作者
|
archive.author=作者
|
||||||
archive.game_version=遊戲版本
|
archive.game_version=遊戲版本
|
||||||
|
archive.name=名稱
|
||||||
archive.version=版本
|
archive.version=版本
|
||||||
|
|
||||||
assets.download=下載資源
|
assets.download=下載資源
|
||||||
@ -251,6 +252,8 @@ mods.add=新增模組
|
|||||||
mods.add.failed=新增模組 %s 失敗。
|
mods.add.failed=新增模組 %s 失敗。
|
||||||
mods.add.success=成功新增模組 %s。
|
mods.add.success=成功新增模組 %s。
|
||||||
mods.choose_mod=選擇模組
|
mods.choose_mod=選擇模組
|
||||||
|
mods.enable=啟用
|
||||||
|
mods.disable=禁用
|
||||||
mods.remove=刪除
|
mods.remove=刪除
|
||||||
|
|
||||||
datapack=資料包
|
datapack=資料包
|
||||||
|
@ -65,6 +65,7 @@ account.username=用户名
|
|||||||
|
|
||||||
archive.author=作者
|
archive.author=作者
|
||||||
archive.game_version=游戏版本
|
archive.game_version=游戏版本
|
||||||
|
archive.name=名称
|
||||||
archive.version=版本
|
archive.version=版本
|
||||||
|
|
||||||
assets.download=下载资源
|
assets.download=下载资源
|
||||||
@ -251,6 +252,8 @@ mods.add=添加模组
|
|||||||
mods.add.failed=添加模组 %s 失败。
|
mods.add.failed=添加模组 %s 失败。
|
||||||
mods.add.success=成功添加模组 %s。
|
mods.add.success=成功添加模组 %s。
|
||||||
mods.choose_mod=选择模组
|
mods.choose_mod=选择模组
|
||||||
|
mods.enable=启用
|
||||||
|
mods.disable=禁用
|
||||||
mods.remove=删除
|
mods.remove=删除
|
||||||
|
|
||||||
datapack=数据包
|
datapack=数据包
|
||||||
|
@ -124,7 +124,6 @@ public final class ModManager {
|
|||||||
for (ModInfo modInfo : modInfos) {
|
for (ModInfo modInfo : modInfos) {
|
||||||
Files.deleteIfExists(modInfo.getFile());
|
Files.deleteIfExists(modInfo.getFile());
|
||||||
}
|
}
|
||||||
refreshMods();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path disableMod(Path file) throws IOException {
|
public Path disableMod(Path file) throws IOException {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user