chore: improve host tree filtering

This commit is contained in:
hstyi
2025-06-14 16:21:00 +08:00
committed by hstyi
parent 3cca89b917
commit 26a06b1c91
11 changed files with 368 additions and 207 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -0,0 +1,6 @@
package app.termora.tree
interface Filter {
fun filter(node: Any): Boolean
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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
}
/**
* 优先级最高
*/