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 implements TimiFXUI.Colorful { /** 正在查找节点监听 */ protected final BooleanBinding findingItem; /** 显示隐藏文件监听 */ protected final BooleanProperty showHide; /** 选择队列,左进左出,右边为深度路径,不为空时表正在查找节点 */ protected final ObservableList selectDeque = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); /** 过滤列表,返回 true 时创建该节点 */ protected final List> 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> items = getSelectionModel().getSelectedItems(); // 没有选择、多选、选的不是文件时禁用 return items == null || items.size() != 1 || items.getFirst().getValue().isFile(); }, getSelectionModel().selectedItemProperty())); menuRefresh.setOnAction(e -> refreshItem(getSelectionModel().getSelectedItem())); // 删除 List roots = List.of(File.listRoots()); menuDestroy.disableProperty().bind(Bindings.createBooleanBinding(() -> { ObservableList> items = getSelectionModel().getSelectedItems(); if (items.isEmpty()) { return true; } for (TreeItem 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 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 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) { 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> files) { if (TimiJava.isEmpty(files)) { return; } new AlertConfirm(TimiFXUI.MULTILINGUAL.text("file.destroy")) { @Override protected void onConfirm() { new RunAsync>() { @Override protected TreeItem call() { List items = files.stream().map(TreeItem::getValue).toList(); for (File file : items) { IO.destroy(file); } // 查找最高级节点刷新 int l = Integer.MAX_VALUE; TreeItem 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 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 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> roots = getRoots(); for (TreeItem 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 itemFilter) { itemFilters.add(itemFilter); } /** * 移除构建节点过滤器 * * @param itemFilter 节点过滤器 */ public void removeItemFilter(CallbackArgReturn 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 { /** 默认构造,此时为占位节点 */ 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 fileItems = new ArrayList<>(); File[] files = getValue().listFiles(); if (files != null) { // 排序 List fileList = Arrays.stream(files).sorted(OS.FileSystem.COMPARATOR_FILE_NAME).toList(); // 过滤 list: for (File file : fileList) { for (CallbackArgReturn 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)); } } }); } } }