mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
chore: improve host tree filtering
This commit is contained in:
276
src/main/java/app/termora/tree/FilterableTreeModel.java
Normal file
276
src/main/java/app/termora/tree/FilterableTreeModel.java
Normal file
@@ -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<Object, FilterNode> 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<Object> 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) {
|
||||
}
|
||||
}
|
||||
@@ -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<FindEverywhereResult> {
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
6
src/main/kotlin/app/termora/tree/Filter.kt
Normal file
6
src/main/kotlin/app/termora/tree/Filter.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package app.termora.tree
|
||||
|
||||
interface Filter {
|
||||
|
||||
fun filter(node: Any): Boolean
|
||||
}
|
||||
@@ -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<TreeModelListener>()
|
||||
private var filters = emptyArray<Function<SimpleTreeNode<*>, Boolean>>()
|
||||
private val mapping = mutableMapOf<TreeNode, ReferenceTreeNode>()
|
||||
|
||||
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<SimpleTreeNode<*>, 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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Host>()
|
||||
var allowMulti = true
|
||||
|
||||
private var filter: Function<HostTreeNode, Boolean> = Function<HostTreeNode, Boolean> { 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<SimpleTreeNode<*>, 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
|
||||
|
||||
@@ -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<Any>()
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先级最高
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user