优化 AdvancedListItem (#5460)

This commit is contained in:
Glavo
2026-02-06 22:48:41 +08:00
committed by GitHub
parent 5b11202a9c
commit 570e1e2029
11 changed files with 118 additions and 152 deletions

View File

@@ -312,14 +312,6 @@ public final class FXUtils {
});
}
public static Node wrap(Node node) {
return limitingSize(node, 30, 20);
}
public static Node wrap(SVG svg) {
return wrap(svg.createIcon(20));
}
private static class ListenerPair<T> {
private final ObservableValue<T> value;
private final ChangeListener<? super T> listener;

View File

@@ -130,20 +130,23 @@ public enum SVG {
public static final double DEFAULT_SIZE = 24;
private final String path;
private final String rawPath;
private String path;
SVG(String path) {
// We move the current point so that SVGPath will treat 0 0 24 24 as the layout bounds
this.path = "M24 24ZM0 0Z" + path;
SVG(String rawPath) {
this.rawPath = rawPath;
}
public String getPath() {
if (path == null)
// We move the current point so that SVGPath will treat 0 0 24 24 as the layout bounds
path = "M24 24ZM0 0Z" + rawPath;
return path;
}
public SVGPath createSVGPath() {
var p = new SVGPath();
p.setContent(path);
p.setContent(getPath());
p.getStyleClass().add("svg");
return p;
}

View File

@@ -21,6 +21,7 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
@@ -76,9 +77,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
FXUtils.installFastTooltip(this, tooltip);
canvas = new Canvas(32, 32);
setLeftGraphic(canvas);
canvas.setMouseTransparent(true);
AdvancedListItem.setAlignment(canvas, Pos.CENTER);
setActionButtonVisible(false);
setLeftGraphic(canvas);
if (account != null) {
this.accountProperty().set(account);

View File

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
@@ -54,7 +53,6 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -130,35 +128,25 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
microsoftItem.setLeftIcon(SVG.MICROSOFT);
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
offlineItem.setLeftIcon(SVG.PERSON);
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.DRESSER));
item.setLeftIcon(SVG.DRESSER);
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
item.setRightGraphic(btnRemove);
item.setRightAction(SVG.CLOSE, () -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null));
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
@@ -206,8 +194,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
addAuthServerItem.setTitle(i18n("account.injector.add"));
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
addAuthServerItem.setActionButtonVisible(false);
addAuthServerItem.setLeftGraphic(wrap(SVG.ADD_CIRCLE));
addAuthServerItem.setLeftIcon(SVG.ADD_CIRCLE);
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}

View File

@@ -71,10 +71,9 @@ public class AdvancedListBox extends ScrollPane {
private AdvancedListItem createNavigationDrawerItem(String title, SVG leftGraphic) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setActionButtonVisible(false);
item.setTitle(title);
if (leftGraphic != null) {
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
item.setLeftIcon(leftGraphic);
}
return item;
}
@@ -101,19 +100,22 @@ public class AdvancedListBox extends ScrollPane {
return add(item);
}
@SuppressWarnings("SuspiciousNameCombination")
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
SVG unselectedGraphic, SVG selectedGraphic) {
AdvancedListItem item = createNavigationDrawerItem(title, null);
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
Node unselectedIcon = unselectedGraphic.createIcon(20);
Node selectedIcon = selectedGraphic.createIcon(20);
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
TransitionPane leftGraphic = new TransitionPane();
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
leftGraphic.setMouseTransparent(true);
leftGraphic.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(leftGraphic, 30);
FXUtils.setLimitHeight(leftGraphic, 20);
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setPadding(Insets.EMPTY);
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
FXUtils.onChange(item.activeProperty(), active ->

View File

@@ -17,105 +17,134 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.util.Pair.pair;
import org.jackhuang.hmcl.ui.SVG;
public class AdvancedListItem extends Control {
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public static final double LEFT_GRAPHIC_SIZE = 32;
public static final double LEFT_ICON_SIZE = 20;
public static final Insets LEFT_ICON_MARGIN = new Insets(0, 6, 0, 6);
public static void setMargin(Node graphic, Insets margin) {
BorderPane.setMargin(graphic, margin);
}
public static void setAlignment(Node graphic, Pos alignment) {
BorderPane.setAlignment(graphic, alignment);
}
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
}
public Node getLeftGraphic() {
return leftGraphic.get();
}
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
public ObjectProperty<Node> leftGraphicProperty() {
return leftGraphic;
}
public Node getLeftGraphic() {
return leftGraphic.get();
}
public void setLeftGraphic(Node leftGraphic) {
this.leftGraphic.set(leftGraphic);
}
public Node getRightGraphic() {
return rightGraphic.get();
public void setLeftIcon(SVG svg) {
Node icon = svg.createIcon(LEFT_ICON_SIZE);
icon.setMouseTransparent(true);
BorderPane.setMargin(icon, LEFT_ICON_MARGIN);
BorderPane.setAlignment(icon, Pos.CENTER);
leftGraphicProperty().set(icon);
}
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
public ObjectProperty<Node> rightGraphicProperty() {
return rightGraphic;
}
public Node getRightGraphic() {
return rightGraphic.get();
}
public void setRightGraphic(Node rightGraphic) {
this.rightGraphic.set(rightGraphic);
}
public String getTitle() {
return title.get();
public void setRightAction(SVG icon, Runnable action) {
var button = new JFXButton();
button.setOnAction(e -> {
action.run();
e.consume();
});
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(14));
setAlignment(button, Pos.CENTER);
setRightGraphic(button);
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public boolean isActive() {
return active.get();
}
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
public BooleanProperty activeProperty() {
return active;
}
public void setActive(boolean active) {
this.active.set(active);
public StringProperty subtitleProperty() {
return subtitle;
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isActionButtonVisible() {
return actionButtonVisible.get();
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
public BooleanProperty activeProperty() {
return active;
}
public BooleanProperty actionButtonVisibleProperty() {
return actionButtonVisible;
public boolean isActive() {
return active.get();
}
public void setActionButtonVisible(boolean actionButtonVisible) {
this.actionButtonVisible.set(actionButtonVisible);
public void setActive(boolean active) {
this.active.set(active);
}
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
@@ -128,32 +157,9 @@ public class AdvancedListItem extends Control {
return onActionProperty().get();
}
private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
@Override
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
public static Pair<Node, ImageView> createImageView(Image image) {
return createImageView(image, 32, 32);
}
public static Pair<Node, ImageView> createImageView(Image image, double width, double height) {
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, width);
FXUtils.setLimitHeight(imageViewContainer, height);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, width, height);
imageView.setPreserveRatio(true);
imageView.setImage(image);
imageViewContainer.getChildren().setAll(imageView);
return pair(imageViewContainer, imageView);
}
}

View File

@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -40,41 +38,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
RipplerContainer container = new RipplerContainer(root);
HBox left = new HBox();
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
left.getChildren().clear();
} else {
left.getChildren().setAll(newGraphic);
}
});
root.setLeft(left);
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
right.getChildren().clear();
} else {
right.getChildren().setAll(newGraphic);
}
});
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
root.leftProperty().bind(skinnable.leftGraphicProperty());
root.rightProperty().bind(skinnable.rightGraphicProperty());
getChildren().setAll(container);
}

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -65,7 +66,6 @@ import java.util.Locale;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -171,16 +171,14 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED));
gameItem.setActionButtonVisible(false);
gameItem.setLeftIcon(SVG.FORMAT_LIST_BULLETED);
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
// forth item in left sidebar
AdvancedListItem downloadItem = new AdvancedListItem();
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD));
downloadItem.setActionButtonVisible(false);
downloadItem.setLeftIcon(SVG.DOWNLOAD);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> {
Controllers.getDownloadPage().showGameDownloads();
@@ -193,8 +191,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
launcherSettingsItem.setActionButtonVisible(false);
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
launcherSettingsItem.setTitle(i18n("settings"));
launcherSettingsItem.setOnAction(e -> {
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
@@ -206,8 +203,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// sixth item in left sidebar
AdvancedListItem terracottaItem = new AdvancedListItem();
terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2));
terracottaItem.setActionButtonVisible(false);
terracottaItem.setLeftIcon(SVG.GRAPH2);
terracottaItem.setTitle(i18n("terracotta"));
terracottaItem.setOnAction(e -> {
if (TerracottaMetadata.PROVIDER != null) {
@@ -249,7 +245,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
setCenter(getSkinnable().getMainPage());
}
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
public void showGameListPopupMenu(Region gameListItem) {
GameListPopupMenu.show(gameListItem,
JFXPopup.PopupVPosition.TOP,
JFXPopup.PopupHPosition.LEFT,

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
@@ -35,7 +36,6 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
public ProfileListItemSkin(ProfileListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
@@ -46,7 +46,9 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
Node left = FXUtils.wrap(SVG.FOLDER);
Node left = SVG.FOLDER.createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);

View File

@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
@@ -26,7 +27,6 @@ import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import java.util.function.Consumer;
@@ -39,14 +39,20 @@ public class GameAdvancedListItem extends AdvancedListItem {
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings("SuspiciousNameCombination")
public GameAdvancedListItem() {
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
this.imageView = new ImageView();
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setPreserveRatio(true);
imageView.setImage(null);
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
setLeftGraphic(imageViewWrapper);
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
setActionButtonVisible(false);
}
private void loadVersion(String version) {

View File

@@ -69,8 +69,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListItem addProfileItem = new AdvancedListItem();
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setActionButtonVisible(false);
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
addProfileItem.setLeftIcon(SVG.ADD_CIRCLE);
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
pane.setFitToWidth(true);