474 lines
13 KiB
Java
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));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|