refactor Navigation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** 导航组件 */
|
||||
package com.imyeyu.fx.ui.components.navigation;
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.imyeyu.fx.ui.examples.view.pages.component;
|
||||
|
||||
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.sidebar.SidebarItem;
|
||||
import com.imyeyu.fx.utils.Column;
|
||||
@@ -10,7 +12,6 @@ import com.imyeyu.inject.annotation.Component;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.layout.GridPane;
|
||||
|
||||
/**
|
||||
@@ -32,40 +33,55 @@ public class NavigationDemo extends AbstractDemoPane {
|
||||
Label labelCommonly = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("commonly"));
|
||||
labelCommonly.setAlignment(Pos.CENTER);
|
||||
Navigation commonly = new Navigation();
|
||||
commonly.add(new ToggleButton("item 0"));
|
||||
commonly.add(new ToggleButton("item 1"));
|
||||
commonly.add(new ToggleButton("item 2"));
|
||||
commonly.setPrefWidth(240);
|
||||
commonly.add(new NavigationItem("item 0"));
|
||||
commonly.add(new NavigationItem("item 1"));
|
||||
commonly.add(new NavigationItem("item 2"));
|
||||
|
||||
// 二级
|
||||
Label labelSecond = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"));
|
||||
labelSecond.setAlignment(Pos.CENTER);
|
||||
Navigation second = new Navigation();
|
||||
second.add(new ToggleButton("item 0"));
|
||||
second.add(new ToggleButton("item 1"));
|
||||
second.add(new ToggleButton("item 2"));
|
||||
second.addGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1"));
|
||||
second.add(new NavigationItem("item 0"));
|
||||
second.add(new NavigationItem("item 1"));
|
||||
second.add(new NavigationItem("item 2"));
|
||||
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"));
|
||||
labelExpanded.setAlignment(Pos.CENTER);
|
||||
Navigation expanded = new Navigation();
|
||||
expanded.add(new ToggleButton("item 0"));
|
||||
expanded.add(new ToggleButton("item 1"));
|
||||
expanded.add(new ToggleButton("item 2"));
|
||||
expanded.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1"));
|
||||
expanded.add(new NavigationItem("item 0"));
|
||||
expanded.add(new NavigationItem("item 1"));
|
||||
expanded.add(new NavigationItem("item 2"));
|
||||
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"));
|
||||
labelMulti.setAlignment(Pos.CENTER);
|
||||
Navigation multi = new Navigation();
|
||||
multi.add(new ToggleButton("item 0"));
|
||||
multi.add(new ToggleButton("item 1"));
|
||||
multi.add(new ToggleButton("item 2"));
|
||||
multi.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1"));
|
||||
multi.addExpandedGroup(TimiFXUI.MULTILINGUAL.text("fx.example.navigation.second"), new ToggleButton("item 0"), new ToggleButton("item 1"));
|
||||
multi.add(new NavigationItem("item 0"));
|
||||
multi.add(new NavigationItem("item 1"));
|
||||
multi.add(new NavigationItem("item 2"));
|
||||
multi.add(
|
||||
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() {{
|
||||
Column col = Column.build().width(120);
|
||||
Column col = Column.build().width(220);
|
||||
getColumnConstraints().addAll(col, col, col, col);
|
||||
getRowConstraints().addAll(Row.build(), Row.build().alwaysPriority());
|
||||
setVgap(5);
|
||||
|
||||
Reference in New Issue
Block a user