refactor Navigation

This commit is contained in:
Timi
2026-06-17 01:13:20 +08:00
parent dbb39b3d2f
commit 0a5bb393cb
6 changed files with 888 additions and 256 deletions

View File

@@ -1,236 +0,0 @@
package com.imyeyu.fx.ui.components;
import com.imyeyu.fx.TimiFX;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.SmoothScroll;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import lombok.Getter;
import java.util.List;
/**
* 纵向导航组件,可实现二级导航,折叠导航
*
* @author 夜雨
* @since 2022-02-17 00:11
*/
public class Navigation extends ScrollPane implements TimiFXUI {
/** 导航数据列表 */
@Getter
protected final ObservableList<ToggleButton> items;
/** 已选中监听 */
protected final ObjectProperty<ToggleButton> selectedItem;
/** 默认构造器 */
public Navigation() {
items = FXCollections.observableArrayList();
selectedItem = new SimpleObjectProperty<>();
VBox root = new VBox();
root.setBorder(Stroke.BOTTOM);
getStyleClass().addAll("navigation", "sp-border");
setMaxWidth(Double.MAX_VALUE);
setVbarPolicy(ScrollBarPolicy.NEVER);
setFitToWidth(true);
setContent(root);
SmoothScroll.scrollPaneV(this);
// ---------- 事件 ----------
ToggleGroup group = new ToggleGroup();
// 主动选择(代码触发)
selectedItem.addListener((obs, o, newSelectedItem) -> group.selectToggle(newSelectedItem));
// 被动选择(操作触发)
group.selectedToggleProperty().addListener((obs, o, toggle) -> {
if (toggle instanceof ToggleButton btn) {
selectedItem.set(btn);
}
});
ObservableList<Node> children = root.getChildren();
// 响应 TimiFXUI
items.addListener((ListChangeListener<ToggleButton>) c -> {
while (c.next()) {
if (c.wasAdded()) {
// 添加
List<? extends ToggleButton> list = c.getAddedSubList();
for (ToggleButton toggleButton : list) {
toggleButton.setMaxWidth(Double.MAX_VALUE);
toggleButton.getStyleClass().setAll(CSS.MINECRAFT, "navigation-button");
toggleButton.addEventFilter(MouseEvent.MOUSE_PRESSED, TimiFX.EVENT_CONSUME_TG_BTN);
if (toggleButton.getProperties().get("OWNER") instanceof TitledPane pane) {
// 存在所属组
if (!children.contains(pane)) {
// 未添加组
children.add(pane);
}
if (pane.getContent() instanceof VBox box) {
box.getChildren().add(toggleButton);
}
} else {
// 单独项
if (!children.isEmpty()) {
if (children.getLast() instanceof TitledPane) {
// 上一项是组导航,添加上边框
toggleButton.getStyleClass().add("after-group");
}
}
children.add(toggleButton);
}
// 归组
group.getToggles().add(toggleButton);
}
return;
}
if (c.wasRemoved()) {
// 移除
List<? extends ToggleButton> list = c.getRemoved();
for (ToggleButton toggleButton : list) {
if (toggleButton.getProperties().get("OWNER") instanceof TitledPane pane) {
// 存在所属组
if (pane.getContent() instanceof VBox box) {
box.getChildren().remove(toggleButton);
if (box.getChildren().isEmpty()) {
// 该组已没有列表项
children.remove(box);
}
}
} else {
// 单独项
children.remove(toggleButton);
}
// 从组移除
group.getToggles().remove(toggleButton);
}
}
}
});
}
/**
* 添加导航按钮
*
* @param buttons 导航按钮
*/
public void add(ToggleButton... buttons) {
getItems().addAll(buttons);
}
/**
* 添加默认没有展开的导航组
*
* @param title 标题
* @param buttons 导航项
* @return 构造的折叠面板
*/
public TitledPane addGroup(String title, ToggleButton... buttons) {
return addGroup(title, false, buttons);
}
/**
* 添加默认展开的导航组
*
* @param title 标题
* @param buttons 导航项
* @return 构造的折叠面板
*/
public TitledPane addExpandedGroup(String title, ToggleButton... buttons) {
return addGroup(title, true, buttons);
}
/**
* 添加导航组
*
* @param title 标题
* @param isExpanded true 为默认展开
* @param buttons 导航项
* @return 构造的折叠面板
*/
public TitledPane addGroup(String title, boolean isExpanded, ToggleButton... buttons) {
TitledPane pane = new TitledPane();
pane.setText(title);
return addGroup(pane, isExpanded, buttons);
}
/**
* 添加导航组
*
* @param pane 所属组
* @param isExpanded true 为默认展开
* @param buttons 导航项
* @return 原折叠面板
*/
public TitledPane addGroup(TitledPane pane, boolean isExpanded, ToggleButton... buttons) {
VBox content = new VBox();
content.setPadding(Insets.EMPTY);
pane.setContent(content);
pane.setExpanded(isExpanded);
pane.getStyleClass().add("group-pane");
for (ToggleButton button : buttons) {
button.getProperties().put("OWNER", pane);
items.add(button);
}
return pane;
}
/**
* 获取该按钮所属组
*
* @param btn 按钮
* @return 所属组null 时为不属于任何组
*/
public TitledPane getGroup(ToggleButton btn) {
if (btn.getProperties().get("OWNER") instanceof TitledPane pane) {
return pane;
}
return null;
}
/**
* 设置当前激活导航项
*
* @param button 导航项
*/
public void setSelectedItem(ToggleButton button) {
selectedItem.set(button);
}
/**
* 获取当前激活的导航项
*
* @return 当前激活的导航项
*/
public ToggleButton getSelectedItem() {
return selectedItem.get();
}
/**
* 获取激活导航项监听
*
* @return 激活导航项监听
*/
public ObjectProperty<ToggleButton> selectedItem() {
return selectedItem;
}
}

View File

@@ -0,0 +1,570 @@
package com.imyeyu.fx.ui.components.navigation;
import com.imyeyu.fx.TimiFX;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.SmoothScroll;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Getter;
import java.util.List;
/**
* 纵向导航组件,可实现多级折叠导航
*
* @author 夜雨
* @since 2026-06-16
*/
public class Navigation extends ScrollPane implements TimiFXUI {
/** 默认缩进像素 */
static final double DEFAULT_INDENT_SIZE = 16;
/** 导航项原始标题属性 */
private static final String RAW_ITEM_TEXT = "RAW_ITEM_TEXT";
/** 导航项原始图标属性 */
private static final String RAW_ITEM_GRAPHIC = "RAW_ITEM_GRAPHIC";
/** 导航项图标同步锁属性 */
private static final String GRAPHIC_SYNCING = "GRAPHIC_SYNCING";
/** 导航项是否已初始化 */
private static final String INITIALIZED = "INITIALIZED";
/** 导航数据列表 */
@Getter
protected final ObservableList<Node> items;
/** 已选中监听 */
protected final ObjectProperty<ToggleButton> selectedItem;
/** 缩进像素 */
private final DoubleProperty indentSize;
/** 根容器 */
private final VBox contentBox;
/** 选择组 */
private final ToggleGroup toggleGroup;
/**
* 默认构造器
*/
public Navigation() {
items = FXCollections.observableArrayList();
selectedItem = new SimpleObjectProperty<>();
indentSize = new SimpleDoubleProperty(DEFAULT_INDENT_SIZE);
contentBox = new VBox();
toggleGroup = new ToggleGroup();
initView();
initSelection();
initPropertyListener();
initItemsListener();
}
/**
* 初始化视图
*/
private void initView() {
contentBox.setBorder(Stroke.BOTTOM);
getStyleClass().addAll("navigation", "sp-border");
setMaxWidth(Double.MAX_VALUE);
setVbarPolicy(ScrollBarPolicy.NEVER);
setFitToWidth(true);
setContent(contentBox);
SmoothScroll.scrollPaneV(this);
}
/**
* 初始化选中同步
*/
private void initSelection() {
selectedItem.addListener((obs, o, newSelectedItem) -> {
if (newSelectedItem == null) {
toggleGroup.selectToggle(null);
return;
}
if (toggleGroup.getToggles().contains(newSelectedItem)) {
toggleGroup.selectToggle(newSelectedItem);
}
});
toggleGroup.selectedToggleProperty().addListener((obs, o, toggle) -> {
if (toggle instanceof ToggleButton button) {
selectedItem.set(button);
return;
}
selectedItem.set(null);
});
}
/**
* 初始化属性监听
*/
private void initPropertyListener() {
indentSize.addListener((obs, o, n) -> rebuild());
}
/**
* 初始化导航数据监听
*/
private void initItemsListener() {
items.addListener(this::handleItemsChanged);
}
/**
* 处理导航数据变更
*
* @param change 变更内容
*/
private void handleItemsChanged(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasAdded()) {
for (Node node : change.getAddedSubList()) {
bindGroup(node);
}
}
if (change.wasRemoved()) {
for (Node node : change.getRemoved()) {
unbindGroup(node);
}
}
}
rebuild();
}
/**
* 绑定导航组刷新回调
*
* @param node 子节点
*/
private void bindGroup(Node node) {
if (node instanceof NavigationGroup group) {
group.setStructureChangedAction(this::rebuild);
}
}
/**
* 解绑导航组刷新回调
*
* @param node 子节点
*/
private void unbindGroup(Node node) {
if (node instanceof NavigationGroup group) {
group.setStructureChangedAction(null);
}
}
/**
* 重建导航结构
*/
private void rebuild() {
contentBox.getChildren().clear();
toggleGroup.getToggles().clear();
for (Node node : items) {
Node attachedNode = buildNode(node, 0);
if (attachedNode != null) {
contentBox.getChildren().add(attachedNode);
}
}
refreshStandaloneItemStyle();
if (selectedItem.get() != null && !toggleGroup.getToggles().contains(selectedItem.get())) {
selectedItem.set(null);
}
if (selectedItem.get() != null) {
toggleGroup.selectToggle(selectedItem.get());
}
}
/**
* 构建节点
*
* @param node 节点
* @param level 层级
* @return 可挂载节点
*/
private Node buildNode(Node node, int level) {
if (node instanceof NavigationGroup group) {
return buildGroup(group, level);
}
if (node instanceof ToggleButton button) {
return buildItem(button, level);
}
return null;
}
/**
* 构建导航组
*
* @param group 导航组
* @param level 层级
* @return 导航组节点
*/
private NavigationGroup buildGroup(NavigationGroup group, int level) {
int currentLevel = level + group.getLevelOffset();
group.applyLevel(currentLevel, getIndentSize());
if (!group.getStyleClass().contains("group-pane")) {
group.getStyleClass().add("group-pane");
}
VBox groupContent = group.getContentBox();
groupContent.getChildren().clear();
for (Node child : group.getItems()) {
Node childNode = buildNode(child, currentLevel + 1);
if (childNode != null) {
groupContent.getChildren().add(childNode);
}
}
return group;
}
/**
* 构建导航项
*
* @param button 导航项
* @param level 层级
* @return 导航项节点
*/
private ToggleButton buildItem(ToggleButton button, int level) {
configureButton(button, level);
toggleGroup.getToggles().add(button);
return button;
}
/**
* 刷新独立导航项样式
*/
private void refreshStandaloneItemStyle() {
List<Node> children = contentBox.getChildren();
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof ToggleButton button) {
button.getStyleClass().remove("after-group");
if (0 < i && children.get(i - 1) instanceof TitledPane) {
button.getStyleClass().add("after-group");
}
}
}
}
/**
* 配置导航项
*
* @param button 导航项
* @param level 层级
*/
private void configureButton(ToggleButton button, int level) {
cacheItemState(button);
button.setText(resolveItemText(button));
button.setMaxWidth(Double.MAX_VALUE);
button.setAlignment(Pos.CENTER_LEFT);
applyButtonIndent(button, Math.max(0, level) * getIndentSize());
button.getStyleClass().setAll(CSS.MINECRAFT, "navigation-button");
if (!Boolean.TRUE.equals(button.getProperties().get(INITIALIZED))) {
button.addEventFilter(MouseEvent.MOUSE_PRESSED, TimiFX.EVENT_CONSUME_TG_BTN);
button.graphicProperty().addListener((obs, o, n) -> {
if (!Boolean.TRUE.equals(button.getProperties().get(GRAPHIC_SYNCING))) {
button.getProperties().put(RAW_ITEM_GRAPHIC, n);
}
});
button.getProperties().put(INITIALIZED, true);
}
}
/**
* 缓存导航项原始状态
*
* @param button 导航项
*/
private void cacheItemState(ToggleButton button) {
if (!(button.getProperties().get(RAW_ITEM_TEXT) instanceof String)) {
button.getProperties().put(RAW_ITEM_TEXT, button.getText());
}
if (!button.getProperties().containsKey(RAW_ITEM_GRAPHIC)) {
button.getProperties().put(RAW_ITEM_GRAPHIC, button.getGraphic());
}
}
/**
* 获取导航项原始标题
*
* @param button 导航项
* @return 原始标题
*/
private String resolveItemText(ToggleButton button) {
cacheItemState(button);
return (String) button.getProperties().get(RAW_ITEM_TEXT);
}
/**
* 应用导航项缩进
*
* @param button 导航项
* @param indentWidth 缩进宽度
*/
private void applyButtonIndent(ToggleButton button, double indentWidth) {
Node rawGraphic = (Node) button.getProperties().get(RAW_ITEM_GRAPHIC);
if (0 == indentWidth) {
button.getProperties().put(GRAPHIC_SYNCING, true);
button.setGraphic(rawGraphic);
button.getProperties().put(GRAPHIC_SYNCING, false);
return;
}
Region spacer = new Region();
spacer.setMinWidth(indentWidth);
spacer.setPrefWidth(indentWidth);
spacer.setMaxWidth(indentWidth);
HBox graphicBox = new HBox();
graphicBox.getChildren().add(spacer);
if (rawGraphic != null) {
graphicBox.getChildren().add(rawGraphic);
}
button.getProperties().put(GRAPHIC_SYNCING, true);
button.setGraphic(graphicBox);
button.getProperties().put(GRAPHIC_SYNCING, false);
}
/**
* 添加导航按钮
*
* @param buttons 导航按钮
*/
public void add(ToggleButton... buttons) {
getItems().addAll(buttons);
}
/**
* 添加导航组
*
* @param groups 导航组
*/
public void add(NavigationGroup... groups) {
getItems().addAll(groups);
}
/**
* 添加导航节点
*
* @param nodes 导航节点
*/
public void add(Node... nodes) {
getItems().addAll(nodes);
}
/**
* 添加默认没有展开的导航组
*
* @param title 标题
* @param buttons 导航项
* @return 构造的导航组
*/
public NavigationGroup addGroup(String title, ToggleButton... buttons) {
return addGroup(title, 0, false, buttons);
}
/**
* 添加默认展开的导航组
*
* @param title 标题
* @param buttons 导航项
* @return 构造的导航组
*/
public NavigationGroup addExpandedGroup(String title, ToggleButton... buttons) {
return addGroup(title, 0, true, buttons);
}
/**
* 添加默认没有展开的导航组
*
* @param title 标题
* @param level 缩进偏移量
* @param buttons 导航项
* @return 构造的导航组
*/
public NavigationGroup addGroup(String title, int level, ToggleButton... buttons) {
return addGroup(title, level, false, buttons);
}
/**
* 添加默认展开的导航组
*
* @param title 标题
* @param level 缩进偏移量
* @param buttons 导航项
* @return 构造的导航组
*/
public NavigationGroup addExpandedGroup(String title, int level, ToggleButton... buttons) {
return addGroup(title, level, true, buttons);
}
/**
* 添加导航组
*
* @param title 标题
* @param level 缩进偏移量
* @param isExpanded true 为默认展开
* @param buttons 导航项
* @return 构造的导航组
*/
public NavigationGroup addGroup(String title, int level, boolean isExpanded, ToggleButton... buttons) {
NavigationGroup group = new NavigationGroup(title).levelOffset(level).expanded(isExpanded);
group.add(buttons);
add(group);
return group;
}
/**
* 添加导航组
*
* @param group 导航组
* @return 原导航组
*/
public NavigationGroup addGroup(NavigationGroup group) {
add(group);
return group;
}
/**
* 添加导航组
*
* @param group 导航组
* @param isExpanded true 为默认展开
* @param buttons 导航项
* @return 原导航组
*/
public NavigationGroup addGroup(NavigationGroup group, boolean isExpanded, ToggleButton... buttons) {
return addGroup(group, group.getLevelOffset(), isExpanded, buttons);
}
/**
* 添加导航组
*
* @param group 导航组
* @param level 缩进偏移量
* @param isExpanded true 为默认展开
* @param buttons 导航项
* @return 原导航组
*/
public NavigationGroup addGroup(NavigationGroup group, int level, boolean isExpanded, ToggleButton... buttons) {
group.levelOffset(level).expanded(isExpanded).add(buttons);
add(group);
return group;
}
/**
* 获取该按钮所属组
*
* @param button 按钮
* @return 所属组null 时为不属于任何组
*/
public NavigationGroup getGroup(ToggleButton button) {
for (Node node : items) {
NavigationGroup group = findGroup(node, button);
if (group != null) {
return group;
}
}
return null;
}
/**
* 设置当前激活导航项
*
* @param button 导航项
*/
public void setSelectedItem(ToggleButton button) {
selectedItem.set(button);
}
/**
* 获取当前激活的导航项
*
* @return 当前激活的导航项
*/
public ToggleButton getSelectedItem() {
return selectedItem.get();
}
/**
* 获取激活导航项监听
*
* @return 激活导航项监听
*/
public ObjectProperty<ToggleButton> selectedItem() {
return selectedItem;
}
/**
* 设置缩进像素
*
* @param indentSize 缩进像素
*/
public void setIndentSize(double indentSize) {
this.indentSize.set(Math.max(0, indentSize));
}
/**
* 获取缩进像素
*
* @return 缩进像素
*/
public double getIndentSize() {
return indentSize.get();
}
/**
* 获取缩进像素属性
*
* @return 缩进像素属性
*/
public DoubleProperty indentSize() {
return indentSize;
}
/**
* 查找按钮所属导航组
*
* @param node 当前节点
* @param button 目标按钮
* @return 所属导航组
*/
private NavigationGroup findGroup(Node node, ToggleButton button) {
if (!(node instanceof NavigationGroup group)) {
return null;
}
for (Node child : group.getItems()) {
if (child == button) {
return group;
}
NavigationGroup nestedGroup = findGroup(child, button);
if (nestedGroup != null) {
return nestedGroup;
}
}
return null;
}
}

View File

@@ -0,0 +1,252 @@
package com.imyeyu.fx.ui.components.navigation;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Getter;
/**
* 导航组
*
* @author 夜雨
* @since 2026-06-16
*/
public class NavigationGroup extends TitledPane {
/** 子节点列表 */
@Getter
private final ObservableList<Node> items;
/** 内容容器 */
private final VBox contentBox;
/** 结构变更回调 */
private Runnable structureChangedAction;
/** 原始标题 */
private String rawTitle;
/** 原始图标 */
private Node rawGraphic;
/** 标题同步锁 */
private boolean textSyncing;
/** 图标同步锁 */
private boolean graphicSyncing;
/** 标题缩进偏移量 */
private int levelOffset;
/**
* 默认构造器
*/
public NavigationGroup() {
items = FXCollections.observableArrayList();
contentBox = new VBox();
structureChangedAction = () -> {};
rawTitle = "";
contentBox.setPadding(Insets.EMPTY);
setContent(contentBox);
getStyleClass().add("group-pane");
items.addListener(this::handleItemsChanged);
initRawStateListener();
}
/**
* 通过标题创建导航组
*
* @param title 标题
*/
public NavigationGroup(String title) {
this();
setText(title);
}
/**
* 添加导航项
*
* @param buttons 导航项
* @return 当前导航组
*/
public NavigationGroup add(ToggleButton... buttons) {
items.addAll(buttons);
return this;
}
/**
* 添加导航组
*
* @param groups 导航组
* @return 当前导航组
*/
public NavigationGroup add(NavigationGroup... groups) {
items.addAll(groups);
return this;
}
/**
* 添加子节点
*
* @param nodes 子节点
* @return 当前导航组
*/
public NavigationGroup add(Node... nodes) {
items.addAll(nodes);
return this;
}
/**
* 设置是否默认展开
*
* @param expanded true 为默认展开
* @return 当前导航组
*/
public NavigationGroup expanded(boolean expanded) {
setExpanded(expanded);
return this;
}
/**
* 设置标题缩进偏移量
*
* @param levelOffset 偏移量
* @return 当前导航组
*/
public NavigationGroup levelOffset(int levelOffset) {
this.levelOffset = Math.max(0, levelOffset);
return this;
}
/**
* 获取内容容器
*
* @return 内容容器
*/
VBox getContentBox() {
return contentBox;
}
/**
* 获取标题缩进偏移量
*
* @return 标题缩进偏移量
*/
int getLevelOffset() {
return levelOffset;
}
/**
* 设置结构变更回调
*
* @param structureChangedAction 回调
*/
void setStructureChangedAction(Runnable structureChangedAction) {
this.structureChangedAction = structureChangedAction == null ? () -> {} : structureChangedAction;
for (Node node : items) {
if (node instanceof NavigationGroup group) {
group.setStructureChangedAction(this::notifyStructureChanged);
}
}
}
/**
* 应用显示层级
*
* @param level 层级
* @param indentSize 缩进像素
*/
void applyLevel(int level, double indentSize) {
textSyncing = true;
super.setText(rawTitle);
textSyncing = false;
applyGraphicIndent(Math.max(0, level) * indentSize);
}
/**
* 处理子节点变更
*
* @param change 变更内容
*/
private void handleItemsChanged(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasAdded()) {
for (Node node : change.getAddedSubList()) {
if (node instanceof NavigationGroup group) {
group.setStructureChangedAction(this::notifyStructureChanged);
}
}
}
if (change.wasRemoved()) {
for (Node node : change.getRemoved()) {
if (node instanceof NavigationGroup group) {
group.setStructureChangedAction(null);
}
}
}
}
notifyStructureChanged();
}
/**
* 触发结构变更通知
*/
private void notifyStructureChanged() {
structureChangedAction.run();
}
/**
* 初始化原始状态监听
*/
private void initRawStateListener() {
rawTitle = getText() == null ? "" : getText();
rawGraphic = getGraphic();
textProperty().addListener((obs, o, n) -> {
if (!textSyncing) {
rawTitle = n == null ? "" : n;
}
});
graphicProperty().addListener((obs, o, n) -> {
if (!graphicSyncing) {
rawGraphic = n;
}
});
}
/**
* 应用标题缩进
*
* @param indentWidth 缩进宽度
*/
private void applyGraphicIndent(double indentWidth) {
if (0 == indentWidth) {
graphicSyncing = true;
super.setGraphic(rawGraphic);
graphicSyncing = false;
return;
}
Region spacer = new Region();
spacer.setMinWidth(indentWidth);
spacer.setPrefWidth(indentWidth);
spacer.setMaxWidth(indentWidth);
HBox graphicBox = new HBox();
graphicBox.getChildren().add(spacer);
if (rawGraphic != null) {
graphicBox.getChildren().add(rawGraphic);
}
graphicSyncing = true;
super.setGraphic(graphicBox);
graphicSyncing = false;
}
}

View File

@@ -0,0 +1,28 @@
package com.imyeyu.fx.ui.components.navigation;
import javafx.scene.control.ToggleButton;
/**
* 导航项
*
* @author 夜雨
* @since 2026-06-16
*/
public class NavigationItem extends ToggleButton {
/**
* 默认构造器
*/
public NavigationItem() {
super();
}
/**
* 通过标题创建导航项
*
* @param text 标题
*/
public NavigationItem(String text) {
super(text);
}
}

View File

@@ -0,0 +1,2 @@
/** 导航组件 */
package com.imyeyu.fx.ui.components.navigation;

View File

@@ -1,7 +1,9 @@
package com.imyeyu.fx.ui.examples.view.pages.component; package com.imyeyu.fx.ui.examples.view.pages.component;
import com.imyeyu.fx.ui.TimiFXUI; import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.ui.components.Navigation; import com.imyeyu.fx.ui.components.navigation.Navigation;
import com.imyeyu.fx.ui.components.navigation.NavigationGroup;
import com.imyeyu.fx.ui.components.navigation.NavigationItem;
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane; import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem; import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
import com.imyeyu.fx.utils.Column; import com.imyeyu.fx.utils.Column;
@@ -10,7 +12,6 @@ import com.imyeyu.inject.annotation.Component;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
/** /**
@@ -32,40 +33,55 @@ public class NavigationDemo extends AbstractDemoPane {
Label labelCommonly = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("commonly")); Label labelCommonly = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("commonly"));
labelCommonly.setAlignment(Pos.CENTER); labelCommonly.setAlignment(Pos.CENTER);
Navigation commonly = new Navigation(); Navigation commonly = new Navigation();
commonly.add(new ToggleButton("item 0")); commonly.setPrefWidth(240);
commonly.add(new ToggleButton("item 1")); commonly.add(new NavigationItem("item 0"));
commonly.add(new ToggleButton("item 2")); commonly.add(new NavigationItem("item 1"));
commonly.add(new NavigationItem("item 2"));
// 二级 // 二级
Label labelSecond = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second")); Label labelSecond = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"));
labelSecond.setAlignment(Pos.CENTER); labelSecond.setAlignment(Pos.CENTER);
Navigation second = new Navigation(); Navigation second = new Navigation();
second.add(new ToggleButton("item 0")); second.add(new NavigationItem("item 0"));
second.add(new ToggleButton("item 1")); second.add(new NavigationItem("item 1"));
second.add(new ToggleButton("item 2")); second.add(new NavigationItem("item 2"));
second.addGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1")); second.add(new NavigationGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"))
.add(new NavigationItem("item 0"), new NavigationItem("item 1")));
// 默认展开 // 默认展开
Label labelExpanded = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.expanded")); Label labelExpanded = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.expanded"));
labelExpanded.setAlignment(Pos.CENTER); labelExpanded.setAlignment(Pos.CENTER);
Navigation expanded = new Navigation(); Navigation expanded = new Navigation();
expanded.add(new ToggleButton("item 0")); expanded.add(new NavigationItem("item 0"));
expanded.add(new ToggleButton("item 1")); expanded.add(new NavigationItem("item 1"));
expanded.add(new ToggleButton("item 2")); expanded.add(new NavigationItem("item 2"));
expanded.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1")); expanded.add(new NavigationGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"))
.expanded(true)
.add(new NavigationItem("item 0"), new NavigationItem("item 1")));
// 多个二 //
Label labelMulti = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.multi")); Label labelMulti = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.multi"));
labelMulti.setAlignment(Pos.CENTER); labelMulti.setAlignment(Pos.CENTER);
Navigation multi = new Navigation(); Navigation multi = new Navigation();
multi.add(new ToggleButton("item 0")); multi.add(new NavigationItem("item 0"));
multi.add(new ToggleButton("item 1")); multi.add(new NavigationItem("item 1"));
multi.add(new ToggleButton("item 2")); multi.add(new NavigationItem("item 2"));
multi.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1")); multi.add(
multi.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1")); new NavigationGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"))
.expanded(true)
.add(
new NavigationItem("item 0"),
new NavigationGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.multi"))
.expanded(true)
.add(new NavigationItem("item 0"), new NavigationItem("item 1"))
),
new NavigationGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"))
.expanded(true)
.add(new NavigationItem("item 0"), new NavigationItem("item 1"))
);
setCenter(new GridPane() {{ setCenter(new GridPane() {{
Column col = Column.build().width(120); Column col = Column.build().width(220);
getColumnConstraints().addAll(col, col, col, col); getColumnConstraints().addAll(col, col, col, col);
getRowConstraints().addAll(Row.build(), Row.build().alwaysPriority()); getRowConstraints().addAll(Row.build(), Row.build().alwaysPriority());
setVgap(5); setVgap(5);