Initial project
This commit is contained in:
473
src/main/java/com/imyeyu/fx/ui/components/FileTreeView.java
Normal file
473
src/main/java/com/imyeyu/fx/ui/components/FileTreeView.java
Normal file
@@ -0,0 +1,473 @@
|
||||
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.get(0).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 (int i = 0; i < items.size(); i++) {
|
||||
if (roots.contains(items.get(i).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 (int i = 0; i < roots.size(); i++) {
|
||||
getRoots().add(new FileItem(roots.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件夹
|
||||
*
|
||||
* @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 (int i = 0; i < items.size(); i++) {
|
||||
IO.destroy(items.get(i));
|
||||
}
|
||||
// 查找最高级节点刷新
|
||||
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.add(0, parent);
|
||||
} while ((parent = parent.getParentFile()) != null);
|
||||
|
||||
if (TimiJava.isNotEmpty(selectDeque)) {
|
||||
ObservableList<TreeItem<File>> roots = getRoots();
|
||||
for (int i = 0; i < roots.size(); i++) {
|
||||
roots.get(i).setExpanded(false);
|
||||
if (roots.get(i).getValue().equals(selectDeque.get(0))) {
|
||||
selectDeque.remove(0);
|
||||
roots.get(i).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 (int i = 0; i < fileList.size(); i++) {
|
||||
for (int j = 0; j < itemFilters.size(); j++) {
|
||||
if (!itemFilters.get(j).handler(fileList.get(i))) {
|
||||
continue list;
|
||||
}
|
||||
}
|
||||
fileItems.add(new FileItem(fileList.get(i)));
|
||||
}
|
||||
}
|
||||
return fileItems;
|
||||
}, items -> {
|
||||
getChildren().setAll(items);
|
||||
|
||||
if (TimiJava.isNotEmpty(selectDeque)) {
|
||||
File file = selectDeque.remove(0);
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i).getValue().equals(file)) {
|
||||
if (items.get(i).getValue().isDirectory()) {
|
||||
items.get(i).setExpanded(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (TimiJava.isEmpty(selectDeque)) {
|
||||
// 执行选中
|
||||
Platform.runLater(() -> scrollTo(getSelectionModel().getSelectedIndex() - 5));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user