将 IconedTwoLineListItem#tags 的类型修改为 ObservableList<Label> (#4473)

This commit is contained in:
Glavo 2025-09-13 20:17:05 +08:00 committed by GitHub
parent 811b1fb5f4
commit e7526e39bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 75 additions and 71 deletions

View File

@ -10,6 +10,7 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
@ -21,7 +22,7 @@ import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final ObservableList<Label> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
@ -62,7 +63,7 @@ public class IconedTwoLineListItem extends HBox {
this.title.set(title);
}
public ObservableList<String> getTags() {
public ObservableList<Label> getTags() {
return tags;
}

View File

@ -29,16 +29,22 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.AggregatedObservableList;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
public static Label createTagLabel(String tag) {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
}
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final ObservableList<Label> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Node> tagLabels;
private final AggregatedObservableList<Node> firstLineChildren;
public TwoLineListItem(String titleString, String subtitleString) {
@ -58,16 +64,9 @@ public class TwoLineListItem extends VBox {
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
});
firstLineChildren = new AggregatedObservableList<>();
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
firstLineChildren.appendList(tagLabels);
firstLineChildren.appendList(tags);
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
Label lblSubtitle = new Label();
@ -111,7 +110,11 @@ public class TwoLineListItem extends VBox {
this.subtitle.set(subtitle);
}
public ObservableList<String> getTags() {
public void addTag(String tag) {
getTags().add(createTagLabel(tag));
}
public ObservableList<Label> getTags() {
return tags;
}

View File

@ -229,27 +229,28 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
} else {
twoLineListItem.setSubtitle(null);
}
twoLineListItem.getTags().clear();
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
switch (versionType) {
case RELEASE:
twoLineListItem.getTags().setAll(i18n("version.game.release"));
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
break;
case PENDING:
case SNAPSHOT:
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.getTags().setAll(i18n("version.game.april_fools"));
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.getTags().setAll(i18n("version.game.snapshot"));
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
break;
default:
twoLineListItem.getTags().setAll(i18n("version.game.old"));
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
break;
}
@ -276,7 +277,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
else
twoLineListItem.getTags().setAll(remoteVersion.getGameVersion());
twoLineListItem.addTag(remoteVersion.getGameVersion());
}
}
}

View File

@ -250,9 +250,9 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
TwoLineListItem item = new TwoLineListItem();
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
item.setSubtitle(java.getBinary().toString());
item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
item.addTag(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
if (vendor != null)
item.getTags().add(i18n("java.info.vendor") + ": " + vendor);
item.addTag(i18n("java.info.vendor") + ": " + vendor);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(item);
root.setCenter(center);

View File

@ -523,9 +523,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
listView.setCellFactory(x -> new FloatListCell<RemoteMod>(listView) {
TwoLineListItem content = new TwoLineListItem();
ImageView imageView = new ImageView();
listView.setCellFactory(x -> new FloatListCell<>(listView) {
private final TwoLineListItem content = new TwoLineListItem();
private final ImageView imageView = new ImageView();
{
HBox container = new HBox(8);
@ -542,10 +542,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(dataItem.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
content.setSubtitle(dataItem.getDescription());
content.getTags().setAll(dataItem.getCategories().stream()
content.getTags().clear();
dataItem.getCategories().stream()
.map(category -> getSkinnable().getLocalizedCategory(category))
.collect(Collectors.toList()));
.forEach(content::addTag);
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
}

View File

@ -228,9 +228,9 @@ public class DownloadPage extends Control implements DecoratorPage {
ModTranslations.Mod mod = getSkinnable().translations.getModByCurseForgeId(getSkinnable().addon.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : getSkinnable().addon.getTitle());
content.setSubtitle(getSkinnable().addon.getDescription());
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
getSkinnable().addon.getCategories().stream()
.map(category -> getSkinnable().page.getLocalizedCategory(category))
.collect(Collectors.toList()));
.forEach(content::addTag);
descriptionPane.getChildren().add(content);
if (getSkinnable().mod != null) {
@ -353,10 +353,9 @@ public class DownloadPage extends Control implements DecoratorPage {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle());
content.setSubtitle(addon.getDescription());
content.getTags().setAll(addon.getCategories().stream()
addon.getCategories().stream()
.map(page::getLocalizedCategory)
.collect(Collectors.toList()));
.forEach(content::addTag);
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
}
@ -389,15 +388,15 @@ public class DownloadPage extends Control implements DecoratorPage {
switch (dataItem.getVersionType()) {
case Alpha:
content.getTags().add(i18n("mods.channel.alpha"));
content.addTag(i18n("mods.channel.alpha"));
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(Theme.blackFill(), 24));
break;
case Beta:
content.getTags().add(i18n("mods.channel.beta"));
content.addTag(i18n("mods.channel.beta"));
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(Theme.blackFill(), 24));
break;
case Release:
content.getTags().add(i18n("mods.channel.release"));
content.addTag(i18n("mods.channel.release"));
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(Theme.blackFill(), 24));
break;
}
@ -405,22 +404,22 @@ public class DownloadPage extends Control implements DecoratorPage {
for (ModLoaderType modLoaderType : dataItem.getLoaders()) {
switch (modLoaderType) {
case FORGE:
content.getTags().add(i18n("install.installer.forge"));
content.addTag(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.getTags().add(i18n("install.installer.cleanroom"));
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.getTags().add(i18n("install.installer.neoforge"));
content.addTag(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.getTags().add(i18n("install.installer.fabric"));
content.addTag(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.getTags().add(i18n("install.installer.liteloader"));
content.addTag(i18n("install.installer.liteloader"));
break;
case QUILT:
content.getTags().add(i18n("install.installer.quilt"));
content.addTag(i18n("install.installer.quilt"));
break;
}
}

View File

@ -47,11 +47,9 @@ public class GameItemSkin extends SkinBase<GameItem> {
TwoLineListItem item = new TwoLineListItem();
item.titleProperty().bind(skinnable.titleProperty());
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
if (StringUtils.isNotBlank(tag)) {
item.getTags().setAll(tag);
} else {
item.getTags().clear();
}
item.getTags().clear();
if (StringUtils.isNotBlank(tag))
item.addTag(tag);
});
item.subtitleProperty().bind(skinnable.subtitleProperty());
BorderPane.setAlignment(item, Pos.CENTER);

View File

@ -395,7 +395,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
title.getTags().setAll(modInfo.getModInfo().getVersion());
title.addTag(modInfo.getModInfo().getVersion());
}
title.setSubtitle(FileUtils.getName(modInfo.getModInfo().getFile()));
@ -442,9 +442,10 @@ class ModListPageSkin extends SkinBase<ModListPage> {
default:
continue;
}
List<String> tags = title.getTags();
if (!tags.contains(loaderName)) {
tags.add(loaderName);
if (title.getTags()
.stream()
.noneMatch(it -> it.getText().equals(loaderName))) {
title.addTag(loaderName);
}
}
@ -551,26 +552,26 @@ class ModListPageSkin extends SkinBase<ModListPage> {
content.getTags().clear();
switch (dataItem.getModInfo().getModLoaderType()) {
case FORGE:
content.getTags().add(i18n("install.installer.forge"));
content.addTag(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.getTags().add(i18n("install.installer.cleanroom"));
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.getTags().add(i18n("install.installer.neoforge"));
content.addTag(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.getTags().add(i18n("install.installer.fabric"));
content.addTag(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.getTags().add(i18n("install.installer.liteloader"));
content.addTag(i18n("install.installer.liteloader"));
break;
case QUILT:
content.getTags().add(i18n("install.installer.quilt"));
content.addTag(i18n("install.installer.quilt"));
break;
}
if (dataItem.getMod() != null && I18n.isUseChinese()) {
content.getTags().add(dataItem.getMod().getDisplayName());
content.addTag(dataItem.getMod().getDisplayName());
}
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {

View File

@ -245,7 +245,8 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
item.setTitle(parseColorEscapes(skinnable.getBackupWorld().getWorldName()));
item.setSubtitle(formatDateTime(skinnable.getBackupTime()) + (skinnable.count == 0 ? "" : " (" + skinnable.count + ")"));
if (world.getGameVersion() != null) item.getTags().add(world.getGameVersion());
if (world.getGameVersion() != null)
item.addTag(world.getGameVersion());
}
{

View File

@ -76,9 +76,9 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
if (world.getGameVersion() != null)
item.getTags().add(world.getGameVersion());
item.addTag(world.getGameVersion());
if (world.isLocked())
item.getTags().add(i18n("world.locked"));
item.addTag(i18n("world.locked"));
}
{

View File

@ -28,10 +28,10 @@ import java.util.function.Function;
public final class AggregatedObservableList<T> {
protected final List<ObservableList<T>> lists = new ArrayList<>();
final private List<Integer> sizes = new ArrayList<>();
final private List<InternalListModificationListener> listeners = new ArrayList<>();
final protected ObservableList<T> aggregatedList = FXCollections.observableArrayList();
private final List<ObservableList<? extends T>> lists = new ArrayList<>();
private final List<Integer> sizes = new ArrayList<>();
private final List<InternalListModificationListener> listeners = new ArrayList<>();
private final ObservableList<T> aggregatedList = FXCollections.observableArrayList();
public AggregatedObservableList() {
@ -46,7 +46,7 @@ public final class AggregatedObservableList<T> {
return aggregatedList;
}
public void appendList(@NotNull ObservableList<T> list) {
public void appendList(@NotNull ObservableList<? extends T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
@ -59,7 +59,7 @@ public final class AggregatedObservableList<T> {
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void prependList(@NotNull ObservableList<T> list) {
public void prependList(@NotNull ObservableList<? extends T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(0, list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
@ -72,7 +72,7 @@ public final class AggregatedObservableList<T> {
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void removeList(@NotNull ObservableList<T> list) {
public void removeList(@NotNull ObservableList<? extends T> list) {
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
final int index = lists.indexOf(list);
@ -98,7 +98,7 @@ public final class AggregatedObservableList<T> {
* @param list the list in question
* @return the start index of this list in the aggregated List
*/
private int getStartIndex(@NotNull ObservableList<T> list) {
private int getStartIndex(@NotNull ObservableList<? extends T> list) {
int startIndex = 0;
//System.out.println("=== searching startIndex of " + list);
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
@ -120,7 +120,7 @@ public final class AggregatedObservableList<T> {
* @param startIndex the start of the list (retrieve with {@link #getStartIndex(ObservableList)}
* @return the end index of this list in the aggregated List
*/
private int getEndIndex(@NotNull ObservableList<T> list, int startIndex) {
private int getEndIndex(@NotNull ObservableList<? extends T> list, int startIndex) {
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int index = lists.indexOf(list);
return startIndex + sizes.get(index) - 1;
@ -129,9 +129,9 @@ public final class AggregatedObservableList<T> {
private final class InternalListModificationListener implements ListChangeListener<T> {
@NotNull
private final ObservableList<T> list;
private final ObservableList<? extends T> list;
public InternalListModificationListener(@NotNull ObservableList<T> list) {
public InternalListModificationListener(@NotNull ObservableList<? extends T> list) {
this.list = list;
}