From 26a06b1c9128cba0ecd4d022597772375ffdfe59 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sat, 14 Jun 2025 16:21:00 +0800 Subject: [PATCH] chore: improve host tree filtering --- .../app/termora/tree/FilterableTreeModel.java | 276 ++++++++++++++++++ src/main/kotlin/app/termora/WelcomePanel.kt | 46 ++- .../app/termora/actions/NewHostAction.kt | 4 +- .../app/termora/keymgr/KeyManagerPanel.kt | 8 +- .../plugin/internal/ssh/SSHHostOptionsPane.kt | 10 +- .../termora/sftp/SFTPFileSystemViewPanel.kt | 19 +- src/main/kotlin/app/termora/tree/Filter.kt | 6 + .../termora/tree/FilterableHostTreeModel.kt | 156 ---------- .../kotlin/app/termora/tree/NewHostTree.kt | 5 + .../app/termora/tree/NewHostTreeDialog.kt | 22 +- ...MoreInfoSimpleTreeCellRendererExtension.kt | 23 +- 11 files changed, 368 insertions(+), 207 deletions(-) create mode 100644 src/main/java/app/termora/tree/FilterableTreeModel.java create mode 100644 src/main/kotlin/app/termora/tree/Filter.kt delete mode 100644 src/main/kotlin/app/termora/tree/FilterableHostTreeModel.kt diff --git a/src/main/java/app/termora/tree/FilterableTreeModel.java b/src/main/java/app/termora/tree/FilterableTreeModel.java new file mode 100644 index 0000000..86e3c15 --- /dev/null +++ b/src/main/java/app/termora/tree/FilterableTreeModel.java @@ -0,0 +1,276 @@ +package app.termora.tree; + +import app.termora.Disposable; +import app.termora.DocumentAdaptor; +import com.formdev.flatlaf.extras.components.FlatTextField; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; + +import javax.swing.JTree; +import javax.swing.event.DocumentEvent; +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FilterableTreeModel implements TreeModel, Disposable { + private final TreeModel originalModel; + private final EventListenerList eventListener = new EventListenerList(); + private Filter[] filters = new Filter[0]; + private final TreeModelListener originalModelListener = new TreeModelListener() { + @Override + public void treeNodesChanged(TreeModelEvent e) { + rebuildAndNotify(e); + } + + @Override + public void treeNodesInserted(TreeModelEvent e) { + rebuildAndNotify(e); + } + + @Override + public void treeNodesRemoved(TreeModelEvent e) { + rebuildAndNotify(e); + } + + @Override + public void treeStructureChanged(TreeModelEvent e) { + rebuildAndNotify(e); + } + }; + private final Map filteredTree = new LinkedHashMap<>(); + private final JTree tree; + + public final FlatTextField filterableTextField = new FlatTextField(); + public boolean expand = false; + + public FilterableTreeModel(JTree tree) { + this.tree = tree; + this.originalModel = tree.getModel(); + this.originalModel.addTreeModelListener(originalModelListener); + + filterableTextField.getDocument().addDocumentListener(new DocumentAdaptor() { + @Override + public void changedUpdate(@NotNull DocumentEvent e) { + rebuildAndNotify(null); + } + }); + + // 初始化构建过滤树 + rebuildFilteredTree(); + } + + @Override + public Object getRoot() { + return originalModel.getRoot(); + } + + @Override + public Object getChild(Object parent, int index) { + return getFilteredChildren(parent)[index]; + } + + @Override + public int getChildCount(Object parent) { + return getFilteredChildren(parent).length; + } + + @Override + public boolean isLeaf(Object node) { + // 如果原模型中是叶子节点,直接返回 + if (originalModel.isLeaf(node)) { + return true; + } + // 如果不是叶子节点,但过滤后没有子节点,也算是叶子节点 + return getFilteredChildren(node).length == 0; + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + // originalModel.valueForPathChanged(path, newValue); + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + return ArrayUtils.indexOf(getFilteredChildren(parent), child); + } + + @Override + public void addTreeModelListener(TreeModelListener l) { + eventListener.add(TreeModelListener.class, l); + } + + @Override + public void removeTreeModelListener(TreeModelListener l) { + eventListener.remove(TreeModelListener.class, l); + } + + /** + * 重建过滤树并通知监听器 + */ + private void rebuildAndNotify(TreeModelEvent event) { + rebuildFilteredTree(); + notifyTreeStructureChanged(event); + if (expand) { + expandAllNodes(tree, getRoot(), new TreePath(getRoot())); + } + } + + /** + * 递归展开所有有子节点的路径 + */ + private void expandAllNodes(JTree tree, Object node, TreePath path) { + // 展开当前路径 + tree.expandPath(path); + + // 递归展开所有子节点 + Object[] children = getFilteredChildren(node); + for (Object child : children) { + if (!isLeaf(child)) { + TreePath childPath = path.pathByAddingChild(child); + expandAllNodes(tree, child, childPath); + } + } + } + + /** + * 重建过滤后的树结构 + */ + private void rebuildFilteredTree() { + filteredTree.clear(); + buildFilteredTree(getRoot(), null); + } + + /** + * 递归构建过滤后的树结构 + * + * @param node 当前节点 + * @param parent 父节点 + */ + private void buildFilteredTree(Object node, Object parent) { + List filteredChildren = new ArrayList<>(); + + // 获取原始子节点 + int originalChildCount = originalModel.getChildCount(node); + for (int i = 0; i < originalChildCount; i++) { + Object child = originalModel.getChild(node, i); + + if (originalModel.isLeaf(child)) { + // 叶子节点:检查是否通过过滤器 + if (passesFilter(child)) { + filteredChildren.add(child); + } + } else { + // 非叶子节点:递归处理子节点 + buildFilteredTree(child, node); + + // 如果子节点有通过过滤的内容,或者节点本身通过过滤,则包含该节点 + FilterNode childFilterNode = filteredTree.get(child); + if ((childFilterNode != null && childFilterNode.children().length > 0) || passesFilter(child)) { + filteredChildren.add(child); + } + } + } + + // 将当前节点的过滤结果保存 + filteredTree.put(node, new FilterNode( + node, + parent, + filteredChildren.toArray(), + passesFilter(node) + )); + } + + /** + * 检查节点是否通过所有过滤器 + * + * @param node 要检查的节点 + * @return true如果通过所有过滤器 + */ + private boolean passesFilter(Object node) { + return Arrays.stream(filters).allMatch(filter -> filter.filter(node)); + } + + /** + * 获取节点的过滤后子节点 + * + * @param parent 父节点 + * @return 过滤后的子节点数组 + */ + private Object[] getFilteredChildren(Object parent) { + FilterNode filterNode = filteredTree.get(parent); + if (filterNode == null) { + return new Object[0]; + } + return filterNode.children(); + } + + /** + * 通知所有监听器树结构已改变 + */ + private void notifyTreeStructureChanged(TreeModelEvent event) { + TreeModelListener[] listeners = eventListener.getListeners(TreeModelListener.class); + if (listeners.length > 0) { + TreeModelEvent evt = new TreeModelEvent(this, event == null ? new Object[]{getRoot()} : event.getPath()); + for (TreeModelListener listener : listeners) { + listener.treeStructureChanged(evt); + } + } + } + + /** + * 添加过滤器 + * + * @param filter 要添加的过滤器 + */ + public void addFilter(Filter filter) { + filters = ArrayUtils.add(filters, filter); + rebuildAndNotify(null); + } + + /** + * 移除过滤器 + * + * @param filter 要移除的过滤器 + */ + public void removeFilter(Filter filter) { + filters = ArrayUtils.removeElement(filters, filter); + rebuildAndNotify(null); + } + + public void filter() { + rebuildAndNotify(null); + } + + /** + * 清除所有过滤器 + */ + public void clearFilters() { + filters = new Filter[0]; + rebuildAndNotify(null); + } + + @Override + public void dispose() { + filters = new Filter[0]; + filteredTree.clear(); + originalModel.removeTreeModelListener(originalModelListener); + } + + /** + * 过滤节点的记录类 + * + * @param node 节点对象 + * @param parent 父节点 + * @param children 过滤后的子节点数组 + * @param matched 节点本身是否匹配过滤条件 + */ + private record FilterNode(Object node, Object parent, Object[] children, boolean matched) { + } +} diff --git a/src/main/kotlin/app/termora/WelcomePanel.kt b/src/main/kotlin/app/termora/WelcomePanel.kt index acb78ad..43cbe99 100644 --- a/src/main/kotlin/app/termora/WelcomePanel.kt +++ b/src/main/kotlin/app/termora/WelcomePanel.kt @@ -7,13 +7,11 @@ import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereResult import app.termora.plugin.internal.ssh.SSHProtocolProvider import app.termora.terminal.DataKey -import app.termora.tree.NewHostTree -import app.termora.tree.NewHostTreeModel +import app.termora.tree.* import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.components.FlatButton -import com.formdev.flatlaf.extras.components.FlatTextField import org.apache.commons.lang3.StringUtils import org.jdesktop.swingx.action.ActionManager import java.awt.BorderLayout @@ -26,19 +24,18 @@ import kotlin.math.max class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab, DataProvider { + private val properties get() = DatabaseManager.getInstance().properties private val rootPanel = JPanel(BorderLayout()) - private val searchTextField = FlatTextField() private val hostTree = NewHostTree() private val bannerPanel = BannerPanel() private val toggle = FlatButton() private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean() private val dataProviderSupport = DataProviderSupport() private val hostTreeModel = hostTree.model as NewHostTreeModel + private val filterableTreeModel = FilterableTreeModel(hostTree).apply { expand = true } private var lastFocused: Component? = null -// private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) { -// searchTextField.text.isBlank() -// } + private val searchTextField = filterableTreeModel.filterableTextField init { initView() @@ -144,7 +141,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() panel.add(scrollPane, BorderLayout.CENTER) panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0) -// hostTree.model = filterableHostTreeModel + hostTree.model = filterableTreeModel hostTree.name = "WelcomeHostTree" hostTree.restoreExpansions() @@ -155,6 +152,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() private fun initEvents() { Disposer.register(this, hostTree) + Disposer.register(hostTree, filterableTreeModel) addComponentListener(object : ComponentAdapter() { override fun componentShown(e: ComponentEvent) { @@ -173,6 +171,18 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() } }) + filterableTreeModel.addFilter(object : Filter { + override fun filter(node: Any): Boolean { + val text = searchTextField.text + if (text.isBlank()) return true + if (node !is HostTreeNode) return false + if (node is TeamTreeNode || node.id == "0") return true + return node.host.name.contains(text) || node.host.host.contains(text) + || node.host.username.contains(text) + } + + }) + FindEverywhereProvider.getFindEverywhereProviders(windowScope).add(object : FindEverywhereProvider { override fun find(pattern: String): List { @@ -202,26 +212,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() } }) - - /*filterableHostTreeModel.addFilter { - if (it !is HostTreeNode) return@addFilter false - val text = searchTextField.text - val host = it.host - text.isBlank() || host.name.contains(text, true) - || host.host.contains(text, true) - || host.username.contains(text, true) - } - - searchTextField.document.addDocumentListener(object : DocumentAdaptor() { - override fun changedUpdate(e: DocumentEvent) { - val text = searchTextField.text - filterableHostTreeModel.refresh() - if (text.isNotBlank()) { - hostTree.expandAll() - } - } - })*/ - searchTextField.addKeyListener(object : KeyAdapter() { private val event = ActionEvent(hostTree, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY) private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) diff --git a/src/main/kotlin/app/termora/actions/NewHostAction.kt b/src/main/kotlin/app/termora/actions/NewHostAction.kt index 904f9f2..4c3cd84 100644 --- a/src/main/kotlin/app/termora/actions/NewHostAction.kt +++ b/src/main/kotlin/app/termora/actions/NewHostAction.kt @@ -1,7 +1,6 @@ package app.termora.actions import app.termora.NewHostDialogV2 -import app.termora.tree.FilterableHostTreeModel import app.termora.tree.HostTreeNode import app.termora.tree.NewHostTreeModel import javax.swing.tree.TreePath @@ -39,8 +38,7 @@ class NewHostAction : AnAction() { ) val newNode = HostTreeNode(host) - val model = if (tree.model is FilterableHostTreeModel) (tree.model as FilterableHostTreeModel).getModel() - else tree.model + val model = tree.model if (model is NewHostTreeModel) { model.insertNodeInto(newNode, lastNode, lastNode.childCount) diff --git a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt index 5ea8424..c15ff29 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt @@ -6,6 +6,7 @@ import app.termora.actions.AnAction import app.termora.actions.AnActionEvent import app.termora.nv.FileChooser import app.termora.plugin.internal.ssh.SSHProtocolProvider +import app.termora.tree.Filter import app.termora.tree.HostTreeNode import app.termora.tree.NewHostTreeDialog import com.formdev.flatlaf.extras.components.FlatComboBox @@ -214,8 +215,11 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay } val owner = SwingUtilities.getWindowAncestor(this) ?: return - val hostTreeDialog = NewHostTreeDialog(owner) - hostTreeDialog.setFilter { it is HostTreeNode && it.host.protocol == SSHProtocolProvider.PROTOCOL } + val hostTreeDialog = NewHostTreeDialog(owner, filter = object : Filter { + override fun filter(node: Any): Boolean { + return node is HostTreeNode && node.host.protocol == SSHProtocolProvider.PROTOCOL + } + }) hostTreeDialog.setTreeName("KeyManagerPanel.SSHCopyIdTree") hostTreeDialog.isVisible = true val hosts = hostTreeDialog.hosts diff --git a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHHostOptionsPane.kt b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHHostOptionsPane.kt index 7debd2c..8386965 100644 --- a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHHostOptionsPane.kt +++ b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHHostOptionsPane.kt @@ -4,6 +4,7 @@ import app.termora.* import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManagerDialog import app.termora.plugin.internal.BasicProxyOption +import app.termora.tree.Filter import app.termora.tree.HostTreeNode import app.termora.tree.NewHostTreeDialog import com.formdev.flatlaf.FlatClientProperties @@ -978,8 +979,13 @@ open class SSHHostOptionsPane : OptionsPane() { private fun initEvents() { addBtn.addActionListener(object : AbstractAction() { override fun actionPerformed(e: ActionEvent?) { - val dialog = NewHostTreeDialog(owner) - dialog.setFilter { node -> node is HostTreeNode && jumpHosts.none { it.id == node.host.id } && filter.invoke(node.host) } + val dialog = NewHostTreeDialog(owner, object : Filter { + override fun filter(node: Any): Boolean { + return node is HostTreeNode && jumpHosts.none { it.id == node.host.id } && filter.invoke( + node.host + ) + } + }) dialog.setTreeName("HostOptionsPane.JumpHostsOption.Tree") dialog.setLocationRelativeTo(owner) dialog.isVisible = true diff --git a/src/main/kotlin/app/termora/sftp/SFTPFileSystemViewPanel.kt b/src/main/kotlin/app/termora/sftp/SFTPFileSystemViewPanel.kt index bf15f24..72002f7 100644 --- a/src/main/kotlin/app/termora/sftp/SFTPFileSystemViewPanel.kt +++ b/src/main/kotlin/app/termora/sftp/SFTPFileSystemViewPanel.kt @@ -10,8 +10,7 @@ import app.termora.protocol.FileObjectHandler import app.termora.protocol.FileObjectRequest import app.termora.protocol.TransferProtocolProvider import app.termora.terminal.DataKey -import app.termora.tree.NewHostTree -import app.termora.tree.TreeUtils +import app.termora.tree.* import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.layout.FormLayout @@ -275,12 +274,25 @@ class SFTPFileSystemViewPanel( private fun initView() { tree.contextmenu = false tree.dragEnabled = false + tree.isRootVisible = false tree.doubleClickConnection = false + tree.showsRootHandles = true val scrollPane = JScrollPane(tree) scrollPane.border = BorderFactory.createEmptyBorder(4, 4, 4, 4) add(scrollPane, BorderLayout.CENTER) + val filterableTreeModel = FilterableTreeModel(tree) + filterableTreeModel.addFilter(object : Filter { + override fun filter(node: Any): Boolean { + if (node !is HostTreeNode) return false + return TransferProtocolProvider.valueOf(node.host.protocol) != null + } + }) + filterableTreeModel.filter() + tree.model = filterableTreeModel + Disposer.register(tree, filterableTreeModel) + TreeUtils.loadExpansionState(tree, properties.getString("SFTPTabbed.Tree.state", StringUtils.EMPTY)) } @@ -310,9 +322,6 @@ class SFTPFileSystemViewPanel( }) } - override fun dispose() { - properties.putString("SFTPTabbed.Tree.state", TreeUtils.saveExpansionState(tree)) - } } @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/app/termora/tree/Filter.kt b/src/main/kotlin/app/termora/tree/Filter.kt new file mode 100644 index 0000000..ccda6e1 --- /dev/null +++ b/src/main/kotlin/app/termora/tree/Filter.kt @@ -0,0 +1,6 @@ +package app.termora.tree + +interface Filter { + + fun filter(node: Any): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/tree/FilterableHostTreeModel.kt b/src/main/kotlin/app/termora/tree/FilterableHostTreeModel.kt deleted file mode 100644 index 9908d5c..0000000 --- a/src/main/kotlin/app/termora/tree/FilterableHostTreeModel.kt +++ /dev/null @@ -1,156 +0,0 @@ -package app.termora.tree - -import org.apache.commons.lang3.ArrayUtils -import java.util.function.Function -import javax.swing.JTree -import javax.swing.SwingUtilities -import javax.swing.event.TreeModelEvent -import javax.swing.event.TreeModelListener -import javax.swing.tree.DefaultMutableTreeNode -import javax.swing.tree.TreeModel -import javax.swing.tree.TreeNode -import javax.swing.tree.TreePath - -class FilterableHostTreeModel( - private val tree: JTree, - /** - * 如果返回 true 则空文件夹也展示 - */ - private val showEmptyFolder: () -> Boolean = { true } -) : TreeModel { - private val model = tree.model - private val root = ReferenceTreeNode(model.root) - private var listeners = emptyArray() - private var filters = emptyArray, Boolean>>() - private val mapping = mutableMapOf() - - init { - refresh() - initEvents() - } - - - /** - * @param a 旧的 - * @param b 新的 - */ - private fun cloneTree(a: SimpleTreeNode<*>, b: DefaultMutableTreeNode) { - b.removeAllChildren() - for (c in a.children()) { - if (c !is SimpleTreeNode<*>) { - continue - } - - if (c.isFolder.not()) { - if (filters.isNotEmpty() && filters.none { it.apply(c) }) { - continue - } - } - - val n = ReferenceTreeNode(c).apply { mapping[c] = this }.apply { b.add(this) } - - // 文件夹递归复制 - if (c.isFolder) { - cloneTree(c, n) - } - - // 如果是文件夹 - if (c.isFolder) { - if (n.childCount == 0) { - if (showEmptyFolder.invoke()) { - continue - } - n.removeFromParent() - } - } - } - } - - private fun initEvents() { - model.addTreeModelListener(object : TreeModelListener { - override fun treeNodesChanged(e: TreeModelEvent) { - refresh() - } - - override fun treeNodesInserted(e: TreeModelEvent) { - refresh() - } - - override fun treeNodesRemoved(e: TreeModelEvent) { - refresh() - } - - override fun treeStructureChanged(e: TreeModelEvent) { - refresh() - } - }) - } - - override fun getRoot(): Any { - return root.userObject - } - - override fun getChild(parent: Any, index: Int): Any { - val c = map(parent)?.getChildAt(index) - if (c is ReferenceTreeNode) { - return c.userObject - } - throw IndexOutOfBoundsException("Index out of bounds") - } - - override fun getChildCount(parent: Any): Int { - return map(parent)?.childCount ?: 0 - } - - private fun map(parent: Any): ReferenceTreeNode? { - if (parent is TreeNode) { - return mapping[parent] - } - return null - } - - override fun isLeaf(node: Any?): Boolean { - return (node as TreeNode).isLeaf - } - - override fun valueForPathChanged(path: TreePath, newValue: Any) { - - } - - override fun getIndexOfChild(parent: Any, child: Any): Int { - val c = map(parent) ?: return -1 - for (i in 0 until c.childCount) { - val e = c.getChildAt(i) - if (e is ReferenceTreeNode && e.userObject == child) { - return i - } - } - return -1 - } - - override fun addTreeModelListener(l: TreeModelListener) { - listeners = ArrayUtils.addAll(listeners, l) - } - - override fun removeTreeModelListener(l: TreeModelListener) { - listeners = ArrayUtils.removeElement(listeners, l) - } - - fun addFilter(f: Function, Boolean>) { - filters = ArrayUtils.add(filters, f) - } - - fun refresh() { - mapping.clear() - mapping[model.root as TreeNode] = root - cloneTree(model.root as HostTreeNode, root) - SwingUtilities.updateComponentTreeUI(tree) - } - - fun getModel(): TreeModel { - return model - } - - private class ReferenceTreeNode(any: Any) : DefaultMutableTreeNode(any) - -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/tree/NewHostTree.kt b/src/main/kotlin/app/termora/tree/NewHostTree.kt index 61d44a0..cda12e7 100644 --- a/src/main/kotlin/app/termora/tree/NewHostTree.kt +++ b/src/main/kotlin/app/termora/tree/NewHostTree.kt @@ -39,6 +39,7 @@ import javax.swing.event.PopupMenuListener import javax.swing.event.TreeModelEvent import javax.swing.event.TreeModelListener import javax.swing.filechooser.FileNameExtensionFilter +import javax.swing.tree.TreeModel import javax.swing.tree.TreePath import javax.swing.tree.TreeSelectionModel import javax.xml.parsers.DocumentBuilderFactory @@ -88,6 +89,9 @@ class NewHostTree : SimpleTree(), Disposable { initEvents() } + fun getSuperModel(): TreeModel { + return super.getModel() + } private fun initViews() { super.setModel(model) @@ -390,6 +394,7 @@ class NewHostTree : SimpleTree(), Disposable { override fun treeStructureChanged(e: TreeModelEvent) { SwingUtilities.updateComponentTreeUI(tree) } + } } diff --git a/src/main/kotlin/app/termora/tree/NewHostTreeDialog.kt b/src/main/kotlin/app/termora/tree/NewHostTreeDialog.kt index 4d1d1c3..171b3eb 100644 --- a/src/main/kotlin/app/termora/tree/NewHostTreeDialog.kt +++ b/src/main/kotlin/app/termora/tree/NewHostTreeDialog.kt @@ -7,16 +7,19 @@ import java.awt.Dimension import java.awt.Window import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.util.function.Function import javax.swing.* class NewHostTreeDialog( owner: Window, + filter: Filter = object : Filter { + override fun filter(node: Any): Boolean { + return true + } + } ) : DialogWrapper(owner) { var hosts = emptyList() var allowMulti = true - private var filter: Function = Function { true } private val tree = NewHostTree() init { @@ -29,6 +32,12 @@ class NewHostTreeDialog( tree.contextmenu = false tree.doubleClickConnection = false tree.dragEnabled = false + tree.showsRootHandles = true + + val model = FilterableTreeModel(tree) + model.addFilter(filter) + tree.model = model + tree.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { @@ -41,19 +50,13 @@ class NewHostTreeDialog( }) Disposer.register(disposable, tree) + Disposer.register(tree, model) init() setLocationRelativeTo(owner) } - fun setFilter(filter: Function, Boolean>) { - tree.model = FilterableHostTreeModel(tree) { false }.apply { - addFilter(filter) - refresh() - } - } - override fun createCenterPanel(): JComponent { val scrollPane = JScrollPane(tree) scrollPane.border = BorderFactory.createCompoundBorder( @@ -72,7 +75,6 @@ class NewHostTreeDialog( override fun doOKAction() { hosts = tree.getSelectionSimpleTreeNodes(true) - .filter { filter.apply(it) } .map { it.host } if (hosts.isEmpty()) return diff --git a/src/main/kotlin/app/termora/tree/ShowMoreInfoSimpleTreeCellRendererExtension.kt b/src/main/kotlin/app/termora/tree/ShowMoreInfoSimpleTreeCellRendererExtension.kt index 59af753..1615353 100644 --- a/src/main/kotlin/app/termora/tree/ShowMoreInfoSimpleTreeCellRendererExtension.kt +++ b/src/main/kotlin/app/termora/tree/ShowMoreInfoSimpleTreeCellRendererExtension.kt @@ -47,7 +47,7 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple var text = StringUtils.EMPTY if (node.isFolder) { - text = "(${node.getAllChildren().size})" + text = "(${getChildrenCount(tree, node)})" } else if (node is HostTreeNode) { val host = node.host if (host.protocol == SSHProtocolProvider.PROTOCOL || host.protocol == RDPProtocolProvider.PROTOCOL) { @@ -64,6 +64,27 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple return listOf(MyMarkerSimpleTreeCellAnnotation(text)) } + private fun getChildrenCount(tree: JTree, node: SimpleTreeNode<*>): Int { + if (tree is NewHostTree) { + val model = tree.getSuperModel() + var count = 0 + + val queue = ArrayDeque() + queue.add(node) + while (queue.isNotEmpty()) { + val e = queue.removeFirst() + val childrenCount = model.getChildCount(e) + for (i in 0 until childrenCount) { + queue.addLast(model.getChild(e, i)) + } + count++ + } + + return count - 1 + } + return node.getAllChildren().size + } + /** * 优先级最高 */