Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 98 additions & 31 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
Expand All @@ -30,6 +32,7 @@
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
Expand Down Expand Up @@ -221,7 +224,7 @@ private void navigateTo(DirItem item) {
getItems().addAll(item.children);
}

abstract class Item extends Control implements Comparable<Item> {
abstract sealed class Item implements Comparable<Item> {
Comment thread
Glavo marked this conversation as resolved.
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sealed class Item is missing a permits clause. Sealed classes in Java must explicitly list the classes that are allowed to extend them using the permits keyword. Add permits BackItem, DirItem, LitematicFileItem to specify which inner classes can extend this sealed class.

Suggested change
abstract sealed class Item implements Comparable<Item> {
abstract sealed class Item implements Comparable<Item> permits BackItem, DirItem, LitematicFileItem {

Copilot uses AI. Check for mistakes.

boolean isDirectory() {
return this instanceof DirItem;
Expand Down Expand Up @@ -258,11 +261,6 @@ public int compareTo(@NotNull SchematicsPage.Item o) {

return this.getName().compareTo(o.getName());
}

@Override
protected Skin<?> createDefaultSkin() {
return new ItemSkin(this);
}
}

private final class BackItem extends Item {
Expand Down Expand Up @@ -438,6 +436,10 @@ SVG getIcon() {
return SVG.SCHEMA;
}

public @Nullable Image getImage() {
return image;
}

Node getIcon(int size) {
if (image == null) {
return super.getIcon(size);
Expand Down Expand Up @@ -540,62 +542,122 @@ private void updateContent(LitematicFile file) {
}
}

private static final class ItemSkin extends SkinBase<Item> {
public ItemSkin(Item item) {
super(item);
private static final class Cell extends ListCell<Item> {

BorderPane root = new BorderPane();
private final RipplerContainer graphics;
private final BorderPane root;
private final StackPane left;
private final TwoLineListItem center;
private final HBox right;

private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;

private final Tooltip tooltip = new Tooltip();

public Cell() {
this.root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));

{
StackPane left = new StackPane();
left.setMaxSize(32, 32);
left.setPrefSize(32, 32);
left.getChildren().add(item.getIcon(24));
this.left = new StackPane();
left.setPadding(new Insets(0, 8, 0, 0));

Path path = item.getPath();
if (path != null) {
FXUtils.installSlowTooltip(left, path.toAbsolutePath().normalize().toString());
}
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);

this.iconSVG = new SVGPath();
Comment thread
Glavo marked this conversation as resolved.
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);

this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);

BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
}

{
TwoLineListItem center = new TwoLineListItem();
center.setTitle(item.getName());
center.setSubtitle(item.getDescription());

this.center = new TwoLineListItem();
root.setCenter(center);
}

if (!(item instanceof BackItem)) {
HBox right = new HBox(8);
{
this.right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);

JFXButton btnReveal = new JFXButton();
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> item.onReveal());
btnReveal.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem))
item.onReveal();
});

JFXButton btnDelete = new JFXButton();
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
btnDelete.setOnAction(event ->
btnDelete.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem)) {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
item::onDelete, null));
item::onDelete, null);
}
});

right.getChildren().setAll(btnReveal, btnDelete);
root.setRight(right);
}

RipplerContainer container = new RipplerContainer(root);
FXUtils.onClicked(container, item::onClick);
this.getChildren().add(container);
this.graphics = new RipplerContainer(root);
FXUtils.onClicked(graphics, () -> {
Item item = getItem();
if (item != null)
item.onClick();
});
}

@Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);

iconImageView.setImage(null);

if (empty || item == null) {
setGraphic(null);
center.setTitle("");
center.setSubtitle("");
} else {
if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) {
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
}

center.setTitle(item.getName());
center.setSubtitle(item.getDescription());

Path path = item.getPath();
if (path != null) {
tooltip.setText(FileUtils.getAbsolutePath(path));
FXUtils.installSlowTooltip(left, tooltip);
} else {
tooltip.setText("");
Tooltip.uninstall(left, tooltip);
}
Comment on lines +632 to +655
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tooltip is being installed and uninstalled repeatedly in updateItem, which can cause memory leaks and performance issues. The tooltip should be installed once in the constructor (similar to the WorldListCell pattern at line 254 in WorldListPage.java), and only the text should be updated in updateItem. When the item is empty or has no path, set the tooltip text to an empty string instead of uninstalling it. Additionally, ensure that tooltip.setText is called in the empty branch at line 635 to clear the tooltip text when the cell is reused.

Copilot uses AI. Check for mistakes.

root.setRight(item instanceof BackItem ? null : right);

setGraphic(graphics);
}
}
}

Expand All @@ -612,5 +674,10 @@ protected List<Node> initializeToolbar(SchematicsPage skinnable) {
createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory)
);
}

@Override
protected ListCell<Item> createListCell(JFXListView<Item> listView) {
return new Cell();
}
}
}
Loading