Files
timi-fx-ui/src/main/java/com/imyeyu/fx/ui/components/FileTreeView.java
2026-05-11 21:38:37 +08:00

474 lines
13 KiB
Java

package com.imyeyu.fx.ui.components;
import com.imyeyu.fx.ObservableUtils;
import com.imyeyu.fx.icon.TimiFXIcon;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.ui.components.alert.AlertButton;
import com.imyeyu.fx.ui.components.alert.AlertConfirm;
import com.imyeyu.fx.ui.components.alert.AlertTextField;
import com.imyeyu.fx.ui.components.alert.AlertTips;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.utils.OS;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.text.Text;
import javax.naming.NoPermissionException;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 文件目录树组件
*
* @author 夜雨
* @since 2022-05-26 14:32
*/
public class FileTreeView extends XTreeView<File> implements TimiFXUI.Colorful {
/** 正在查找节点监听 */
protected final BooleanBinding findingItem;
/** 显示隐藏文件监听 */
protected final BooleanProperty showHide;
/** 选择队列,左进左出,右边为深度路径,不为空时表正在查找节点 */
protected final ObservableList<File> selectDeque = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
/** 过滤列表,返回 true 时创建该节点 */
protected final List<CallbackArgReturn<File, Boolean>> itemFilters;
/** 默认构造器 */
public FileTreeView() {
showHide = new SimpleBooleanProperty(false);
itemFilters = new ArrayList<>();
findingItem = Bindings.isNotEmpty(selectDeque);
disableProperty().bind(findingItem);
setCellFactory(cell -> new TreeCell<>() {
final Label loading = new Label(TimiFXUI.MULTILINGUAL.text("loading"));
final Text iconFile = TimiFXIcon.fromName("FILE");
final Text iconDirectory = TimiFXIcon.fromName("FOLDER");
{
iconFile.fillProperty().bind(Bindings.when(FileTreeView.this.focusedProperty().and(selectedProperty())).then(GRAY_WHITE).otherwise(GRAY));
iconDirectory.fillProperty().bind(Bindings.when(FileTreeView.this.focusedProperty().and(selectedProperty())).then(GRAY_WHITE).otherwise(GRAY));
}
@Override
protected void updateItem(File file, boolean empty) {
super.updateItem(file, empty);
if (empty) {
setText("");
setGraphic(null);
} else {
if (file == null) {
setGraphic(loading);
} else {
if (TimiJava.isEmpty(file.getName())) {
setText(file.toString());
} else {
setText(file.getName());
}
setGraphic(file.isFile() ? iconFile : iconDirectory);
}
}
}
});
// 右键菜单
MenuItem menuMkdir = new MenuItem(TimiFXUI.MULTILINGUAL.text("file.mkdir"), TimiFXIcon.fromName("FOLDER_ADD"));
MenuItem menuRename = new MenuItem(TimiFXUI.MULTILINGUAL.text("rename"));
MenuItem menuRefresh = new MenuItem(TimiFXUI.MULTILINGUAL.text("refresh"), TimiFXIcon.fromName("REFRESH"));
MenuItem menuDestroy = new MenuItem(TimiFXUI.MULTILINGUAL.text("delete"), TimiFXIcon.fromName("FAIL", RED));
setContextMenu(new ContextMenu(menuMkdir, menuRename, menuRefresh, TimiFXUI.sep(), menuDestroy));
// ---------- 事件 ----------
// 新建文件夹
menuMkdir.disableProperty().bind(menuRefresh.disableProperty());
menuMkdir.setOnAction(e -> mkdir(getSelectionModel().getSelectedItem()));
// 重命名
menuRename.disableProperty().bind(ObservableUtils.onlyOnceInList(getSelectionModel().getSelectedItems()).not());
menuRename.setOnAction(e -> rename(getSelectionModel().getSelectedItem()));
// 刷新
menuRefresh.disableProperty().bind(Bindings.createBooleanBinding(() -> {
List<TreeItem<File>> items = getSelectionModel().getSelectedItems();
// 没有选择、多选、选的不是文件时禁用
return items == null || items.size() != 1 || items.getFirst().getValue().isFile();
}, getSelectionModel().selectedItemProperty()));
menuRefresh.setOnAction(e -> refreshItem(getSelectionModel().getSelectedItem()));
// 删除
List<File> roots = List.of(File.listRoots());
menuDestroy.disableProperty().bind(Bindings.createBooleanBinding(() -> {
ObservableList<TreeItem<File>> items = getSelectionModel().getSelectedItems();
if (items.isEmpty()) {
return true;
}
for (TreeItem<File> item : items) {
if (roots.contains(item.getValue())) {
return true;
}
}
return false;
}, getSelectionModel().getSelectedItems()));
menuDestroy.setOnAction(e -> destroy(getSelectionModel().getSelectedItems()));
// 快捷键
addEventFilter(KeyEvent.KEY_RELEASED, e -> {
boolean control = e.isControlDown();
boolean shift = e.isShiftDown();
boolean alt = e.isAltDown();
KeyCode code = e.getCode();
if (!control && !shift && !alt) {
switch (code) {
case F2 -> menuRename.fire();
case F5 -> menuRefresh.fire();
case DELETE -> menuDestroy.fire();
}
}
if (control && shift && !alt && code == KeyCode.N) {
menuMkdir.fire();
}
});
// ---------- 就绪 ----------
// 过滤隐藏文件
CallbackArgReturn<File, Boolean> filterHidden = file -> !file.isHidden();
itemFilters.add(filterHidden);
showHide.addListener((obs, o, n) -> {
if (isShowHide()) {
itemFilters.remove(filterHidden);
} else {
itemFilters.add(filterHidden);
}
});
// 默认磁盘根目录
for (File root : roots) {
getRoots().add(new FileItem(root));
}
}
/**
* 创建文件夹
*
* @param base 基于文件夹
*/
public void mkdir(TreeItem<File> base) {
if (base == null) {
return;
}
AlertTextField alert = new AlertTextField(TimiFXUI.MULTILINGUAL.text("file.mkdir"));
alert.setTips(TimiFXUI.MULTILINGUAL.text("name"));
alert.setOnActionEvent(action -> {
if (action == AlertButton.Action.CONFIRM) {
try {
IO.dir(IO.fitPath(base.getValue().getAbsolutePath()) + alert.getText());
refreshItem(getSelectionModel().getSelectedItem());
} catch (NoPermissionException ex) {
ex.printStackTrace();
}
}
return true;
});
alert.autoSize().showRelativeCenter(getScene().getWindow());
}
/**
* 重命名
*
* @param file 文件或文件夹
*/
public void rename(TreeItem<File> file) {
if (file == null) {
return;
}
AlertTextField alert = new AlertTextField(TimiFXUI.MULTILINGUAL.text("file.rename"));
alert.setTips(TimiFXUI.MULTILINGUAL.text("name"));
alert.setOnActionEvent(action -> {
if (action == AlertButton.Action.CONFIRM) {
IO.rename(getSelectionModel().getSelectedItem().getValue(), alert.getText());
refreshItem(getSelectionModel().getSelectedItem().getParent());
}
return true;
});
alert.autoSize().showRelativeCenter(getScene().getWindow());
}
/**
* 销毁文件
*
* @param files 文件节点列表
*/
public void destroy(List<TreeItem<File>> files) {
if (TimiJava.isEmpty(files)) {
return;
}
new AlertConfirm(TimiFXUI.MULTILINGUAL.text("file.destroy")) {
@Override
protected void onConfirm() {
new RunAsync<TreeItem<File>>() {
@Override
protected TreeItem<File> call() {
List<File> items = files.stream().map(TreeItem::getValue).toList();
for (File file : items) {
IO.destroy(file);
}
// 查找最高级节点刷新
int l = Integer.MAX_VALUE;
TreeItem<File> item = null;
for (int i = 0, j; i < items.size(); i++) {
j = getTreeItemLevel(files.get(i));
if (j < l) {
l = j;
item = files.get(i);
if (item.getParent() != null) {
item = item.getParent();
}
}
}
return item;
}
@Override
protected void onFinish(TreeItem<File> item) {
if (item != null) {
refreshItem(item);
}
}
@Override
protected void onException(Throwable e) {
AlertTips.error(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("file.tips.destroy_fail"));
}
}.start();
}
}.autoSize().showRelativeCenter(getScene().getWindow());
}
/**
* 刷新子节点
*
* @param treeItem 父级节点
*/
public void refreshItem(TreeItem<File> treeItem) {
if (treeItem instanceof FileItem fileItem) {
fileItem.getChildren().clear();
fileItem.getChildren().add(new FileItem());
fileItem.asyncLoadChildren();
}
}
/**
* 刷新子节点
*
* @param fileItem 父级节点
*/
public void refreshItem(FileItem fileItem) {
fileItem.getChildren().clear();
fileItem.getChildren().add(new FileItem());
fileItem.asyncLoadChildren();
}
/**
* 选择目标路径
*
* @param path 路径
*/
public void selectItem(String path) {
if (TimiJava.isEmpty(path)) {
path = "./";
}
selectItem(new File(path));
}
/**
* 选择目标文件
*
* @param file 目标文件
*/
public void selectItem(File file) {
File parent = file.getAbsoluteFile();
if (!parent.exists()) {
parent = new File("./").getAbsoluteFile();
}
do {
selectDeque.addFirst(parent);
} while ((parent = parent.getParentFile()) != null);
if (TimiJava.isNotEmpty(selectDeque)) {
ObservableList<TreeItem<File>> roots = getRoots();
for (TreeItem<File> root : roots) {
root.setExpanded(false);
if (root.getValue().equals(selectDeque.getFirst())) {
selectDeque.removeFirst();
root.setExpanded(true);
if (TimiJava.isEmpty(selectDeque)) {
break;
}
}
}
}
}
/**
* 添加构建节点过滤器,返回 false 时不创建该节点
*
* @param itemFilter 节点过滤器
*/
public void addItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
itemFilters.add(itemFilter);
}
/**
* 移除构建节点过滤器
*
* @param itemFilter 节点过滤器
*/
public void removeItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
itemFilters.remove(itemFilter);
}
/**
* 设置是否显示隐藏文件
*
* @param showHide true 为显示隐藏文件
*/
public void setShowHide(boolean showHide) {
this.showHide.set(showHide);
}
/**
* 当前是否显示隐藏文件
*
* @return true 为显示隐藏文件
*/
public boolean isShowHide() {
return showHide.get();
}
/**
* 获取显示隐藏文件监听
*
* @return 显示隐藏文件监听
*/
public BooleanProperty showHideProperty() {
return showHide;
}
/**
* 获取正在查找节点监听,此时属于被动展开,用于阻止主动展开的加载节点
*
* @return 正在查找节点监听
*/
public BooleanBinding findingItemProperty() {
return findingItem;
}
/**
* 文件节点
*
* @author 夜雨
* @since 2023-03-16 00:23
*/
public final class FileItem extends TreeItem<File> {
/** 默认构造,此时为占位节点 */
FileItem() {
this(null);
}
/**
* 标准构造
*
* @param file 文件
*/
FileItem(File file) {
super(file);
if (file != null && file.isDirectory()) {
getChildren().add(new FileItem(null)); // 占位
}
// 展开
expandedProperty().addListener((obs, o, isExpanded) -> {
if (isExpanded) {
getSelectionModel().clearSelection();
getSelectionModel().select(this);
asyncLoadChildren();
} else {
getChildren().add(new FileItem());
}
});
}
/** 异步加载子节点 */
void asyncLoadChildren() {
RunAsync.callbackReturn(() -> {
List<FileItem> fileItems = new ArrayList<>();
File[] files = getValue().listFiles();
if (files != null) {
// 排序
List<File> fileList = Arrays.stream(files).sorted(OS.FileSystem.COMPARATOR_FILE_NAME).toList();
// 过滤
list:
for (File file : fileList) {
for (CallbackArgReturn<File, Boolean> itemFilter : itemFilters) {
if (!itemFilter.handler(file)) {
continue list;
}
}
fileItems.add(new FileItem(file));
}
}
return fileItems;
}, items -> {
getChildren().setAll(items);
if (TimiJava.isNotEmpty(selectDeque)) {
File file = selectDeque.removeFirst();
for (FileItem item : items) {
if (item.getValue().equals(file)) {
if (item.getValue().isDirectory()) {
item.setExpanded(true);
}
break;
}
}
if (TimiJava.isEmpty(selectDeque)) {
// 执行选中
Platform.runLater(() -> scrollTo(getSelectionModel().getSelectedIndex() - 5));
}
}
});
}
}
}