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.findeverywhere.FindEverywhereResult
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.tree.NewHostTree
|
import app.termora.tree.*
|
||||||
import app.termora.tree.NewHostTreeModel
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.FlatButton
|
import com.formdev.flatlaf.extras.components.FlatButton
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
@@ -26,19 +24,18 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
|
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
|
||||||
DataProvider {
|
DataProvider {
|
||||||
|
|
||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
private val searchTextField = FlatTextField()
|
|
||||||
private val hostTree = NewHostTree()
|
private val hostTree = NewHostTree()
|
||||||
private val bannerPanel = BannerPanel()
|
private val bannerPanel = BannerPanel()
|
||||||
private val toggle = FlatButton()
|
private val toggle = FlatButton()
|
||||||
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
||||||
|
private val filterableTreeModel = FilterableTreeModel(hostTree).apply { expand = true }
|
||||||
private var lastFocused: Component? = null
|
private var lastFocused: Component? = null
|
||||||
// private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
private val searchTextField = filterableTreeModel.filterableTextField
|
||||||
// searchTextField.text.isBlank()
|
|
||||||
// }
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -144,7 +141,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
panel.add(scrollPane, BorderLayout.CENTER)
|
panel.add(scrollPane, BorderLayout.CENTER)
|
||||||
panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
|
panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
|
||||||
|
|
||||||
// hostTree.model = filterableHostTreeModel
|
hostTree.model = filterableTreeModel
|
||||||
hostTree.name = "WelcomeHostTree"
|
hostTree.name = "WelcomeHostTree"
|
||||||
hostTree.restoreExpansions()
|
hostTree.restoreExpansions()
|
||||||
|
|
||||||
@@ -155,6 +152,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
|
||||||
Disposer.register(this, hostTree)
|
Disposer.register(this, hostTree)
|
||||||
|
Disposer.register(hostTree, filterableTreeModel)
|
||||||
|
|
||||||
addComponentListener(object : ComponentAdapter() {
|
addComponentListener(object : ComponentAdapter() {
|
||||||
override fun componentShown(e: ComponentEvent) {
|
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 {
|
FindEverywhereProvider.getFindEverywhereProviders(windowScope).add(object : FindEverywhereProvider {
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
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() {
|
searchTextField.addKeyListener(object : KeyAdapter() {
|
||||||
private val event = ActionEvent(hostTree, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)
|
private val event = ActionEvent(hostTree, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY)
|
||||||
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.NewHostDialogV2
|
import app.termora.NewHostDialogV2
|
||||||
import app.termora.tree.FilterableHostTreeModel
|
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeModel
|
import app.termora.tree.NewHostTreeModel
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
@@ -39,8 +38,7 @@ class NewHostAction : AnAction() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val newNode = HostTreeNode(host)
|
val newNode = HostTreeNode(host)
|
||||||
val model = if (tree.model is FilterableHostTreeModel) (tree.model as FilterableHostTreeModel).getModel()
|
val model = tree.model
|
||||||
else tree.model
|
|
||||||
|
|
||||||
if (model is NewHostTreeModel) {
|
if (model is NewHostTreeModel) {
|
||||||
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import app.termora.actions.AnAction
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.nv.FileChooser
|
import app.termora.nv.FileChooser
|
||||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||||
|
import app.termora.tree.Filter
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
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 owner = SwingUtilities.getWindowAncestor(this) ?: return
|
||||||
val hostTreeDialog = NewHostTreeDialog(owner)
|
val hostTreeDialog = NewHostTreeDialog(owner, filter = object : Filter {
|
||||||
hostTreeDialog.setFilter { it is HostTreeNode && it.host.protocol == SSHProtocolProvider.PROTOCOL }
|
override fun filter(node: Any): Boolean {
|
||||||
|
return node is HostTreeNode && node.host.protocol == SSHProtocolProvider.PROTOCOL
|
||||||
|
}
|
||||||
|
})
|
||||||
hostTreeDialog.setTreeName("KeyManagerPanel.SSHCopyIdTree")
|
hostTreeDialog.setTreeName("KeyManagerPanel.SSHCopyIdTree")
|
||||||
hostTreeDialog.isVisible = true
|
hostTreeDialog.isVisible = true
|
||||||
val hosts = hostTreeDialog.hosts
|
val hosts = hostTreeDialog.hosts
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import app.termora.tree.Filter
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -978,8 +979,13 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = NewHostTreeDialog(owner)
|
val dialog = NewHostTreeDialog(owner, object : Filter {
|
||||||
dialog.setFilter { node -> node is HostTreeNode && jumpHosts.none { it.id == node.host.id } && filter.invoke(node.host) }
|
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.setTreeName("HostOptionsPane.JumpHostsOption.Tree")
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import app.termora.protocol.FileObjectHandler
|
|||||||
import app.termora.protocol.FileObjectRequest
|
import app.termora.protocol.FileObjectRequest
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.tree.NewHostTree
|
import app.termora.tree.*
|
||||||
import app.termora.tree.TreeUtils
|
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -275,12 +274,25 @@ class SFTPFileSystemViewPanel(
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
tree.contextmenu = false
|
tree.contextmenu = false
|
||||||
tree.dragEnabled = false
|
tree.dragEnabled = false
|
||||||
|
tree.isRootVisible = false
|
||||||
tree.doubleClickConnection = false
|
tree.doubleClickConnection = false
|
||||||
|
tree.showsRootHandles = true
|
||||||
|
|
||||||
val scrollPane = JScrollPane(tree)
|
val scrollPane = JScrollPane(tree)
|
||||||
scrollPane.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
scrollPane.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
add(scrollPane, BorderLayout.CENTER)
|
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))
|
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")
|
@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.TreeModelEvent
|
||||||
import javax.swing.event.TreeModelListener
|
import javax.swing.event.TreeModelListener
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
|
import javax.swing.tree.TreeModel
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
import javax.swing.tree.TreeSelectionModel
|
import javax.swing.tree.TreeSelectionModel
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
@@ -88,6 +89,9 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSuperModel(): TreeModel {
|
||||||
|
return super.getModel()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
super.setModel(model)
|
super.setModel(model)
|
||||||
@@ -390,6 +394,7 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
override fun treeStructureChanged(e: TreeModelEvent) {
|
override fun treeStructureChanged(e: TreeModelEvent) {
|
||||||
SwingUtilities.updateComponentTreeUI(tree)
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ import java.awt.Dimension
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.util.function.Function
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class NewHostTreeDialog(
|
class NewHostTreeDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
|
filter: Filter = object : Filter {
|
||||||
|
override fun filter(node: Any): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
) : DialogWrapper(owner) {
|
) : DialogWrapper(owner) {
|
||||||
var hosts = emptyList<Host>()
|
var hosts = emptyList<Host>()
|
||||||
var allowMulti = true
|
var allowMulti = true
|
||||||
|
|
||||||
private var filter: Function<HostTreeNode, Boolean> = Function<HostTreeNode, Boolean> { true }
|
|
||||||
private val tree = NewHostTree()
|
private val tree = NewHostTree()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -29,6 +32,12 @@ class NewHostTreeDialog(
|
|||||||
tree.contextmenu = false
|
tree.contextmenu = false
|
||||||
tree.doubleClickConnection = false
|
tree.doubleClickConnection = false
|
||||||
tree.dragEnabled = false
|
tree.dragEnabled = false
|
||||||
|
tree.showsRootHandles = true
|
||||||
|
|
||||||
|
val model = FilterableTreeModel(tree)
|
||||||
|
model.addFilter(filter)
|
||||||
|
tree.model = model
|
||||||
|
|
||||||
|
|
||||||
tree.addMouseListener(object : MouseAdapter() {
|
tree.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
@@ -41,19 +50,13 @@ class NewHostTreeDialog(
|
|||||||
})
|
})
|
||||||
|
|
||||||
Disposer.register(disposable, tree)
|
Disposer.register(disposable, tree)
|
||||||
|
Disposer.register(tree, model)
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setLocationRelativeTo(owner)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFilter(filter: Function<SimpleTreeNode<*>, Boolean>) {
|
|
||||||
tree.model = FilterableHostTreeModel(tree) { false }.apply {
|
|
||||||
addFilter(filter)
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val scrollPane = JScrollPane(tree)
|
val scrollPane = JScrollPane(tree)
|
||||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
@@ -72,7 +75,6 @@ class NewHostTreeDialog(
|
|||||||
|
|
||||||
override fun doOKAction() {
|
override fun doOKAction() {
|
||||||
hosts = tree.getSelectionSimpleTreeNodes(true)
|
hosts = tree.getSelectionSimpleTreeNodes(true)
|
||||||
.filter { filter.apply(it) }
|
|
||||||
.map { it.host }
|
.map { it.host }
|
||||||
|
|
||||||
if (hosts.isEmpty()) return
|
if (hosts.isEmpty()) return
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
|
|||||||
var text = StringUtils.EMPTY
|
var text = StringUtils.EMPTY
|
||||||
|
|
||||||
if (node.isFolder) {
|
if (node.isFolder) {
|
||||||
text = "(${node.getAllChildren().size})"
|
text = "(${getChildrenCount(tree, node)})"
|
||||||
} else if (node is HostTreeNode) {
|
} else if (node is HostTreeNode) {
|
||||||
val host = node.host
|
val host = node.host
|
||||||
if (host.protocol == SSHProtocolProvider.PROTOCOL || host.protocol == RDPProtocolProvider.PROTOCOL) {
|
if (host.protocol == SSHProtocolProvider.PROTOCOL || host.protocol == RDPProtocolProvider.PROTOCOL) {
|
||||||
@@ -64,6 +64,27 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
|
|||||||
return listOf(MyMarkerSimpleTreeCellAnnotation(text))
|
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