mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: refactoring HostTree & support sorting (#285)
This commit is contained in:
156
src/main/kotlin/app/termora/FilterableHostTreeModel.kt
Normal file
156
src/main/kotlin/app/termora/FilterableHostTreeModel.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
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<HostTreeNode, Boolean>>()
|
||||||
|
private val mapping = mutableMapOf<TreeNode, ReferenceTreeNode>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param a 旧的
|
||||||
|
* @param b 新的
|
||||||
|
*/
|
||||||
|
private fun cloneTree(a: HostTreeNode, b: DefaultMutableTreeNode) {
|
||||||
|
b.removeAllChildren()
|
||||||
|
for (c in a.children()) {
|
||||||
|
if (c !is HostTreeNode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.host.protocol != Protocol.Folder) {
|
||||||
|
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val n = ReferenceTreeNode(c).apply { mapping[c] = this }.apply { b.add(this) }
|
||||||
|
|
||||||
|
// 文件夹递归复制
|
||||||
|
if (c.host.protocol == Protocol.Folder) {
|
||||||
|
cloneTree(c, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是文件夹
|
||||||
|
if (c.host.protocol == Protocol.Folder) {
|
||||||
|
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<HostTreeNode, 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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -260,7 +260,7 @@ data class Host(
|
|||||||
val tunnelings: List<Tunneling> = emptyList(),
|
val tunnelings: List<Tunneling> = emptyList(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序
|
* 排序,越小越靠前
|
||||||
*/
|
*/
|
||||||
val sort: Long = 0,
|
val sort: Long = 0,
|
||||||
/**
|
/**
|
||||||
@@ -307,4 +307,8 @@ data class Host(
|
|||||||
result = 31 * result + ownerId.hashCode()
|
result = 31 * result + ownerId.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import java.util.*
|
import org.slf4j.LoggerFactory
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
interface HostListener : EventListener {
|
|
||||||
fun hostAdded(host: Host) {}
|
|
||||||
fun hostRemoved(id: String) {}
|
|
||||||
fun hostsChanged() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HostManager private constructor() {
|
class HostManager private constructor() {
|
||||||
@@ -14,42 +9,29 @@ class HostManager private constructor() {
|
|||||||
fun getInstance(): HostManager {
|
fun getInstance(): HostManager {
|
||||||
return ApplicationScope.forApplicationScope().getOrCreate(HostManager::class) { HostManager() }
|
return ApplicationScope.forApplicationScope().getOrCreate(HostManager::class) { HostManager() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(HostManager::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
private val listeners = mutableListOf<HostListener>()
|
|
||||||
|
|
||||||
fun addHost(host: Host, notify: Boolean = true) {
|
fun addHost(host: Host) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
database.addHost(host)
|
database.addHost(host)
|
||||||
if (notify) listeners.forEach { it.hostAdded(host) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeHost(id: String) {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
database.removeHost(id)
|
|
||||||
listeners.forEach { it.hostRemoved(id) }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hosts(): List<Host> {
|
fun hosts(): List<Host> {
|
||||||
return database.getHosts()
|
val hosts: List<Host>
|
||||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
measureTimeMillis {
|
||||||
|
hosts = database.getHosts()
|
||||||
|
.filter { !it.deleted }
|
||||||
|
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||||
|
}.let {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("hosts: $it ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAll() {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
database.removeAllHost()
|
|
||||||
listeners.forEach { it.hostsChanged() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addHostListener(listener: HostListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeHostListener(listener: HostListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1134,16 +1134,16 @@ open class HostOptionsPane : 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 = HostTreeDialog(owner) { host ->
|
val dialog = NewHostTreeDialog(owner)
|
||||||
jumpHosts.none { it.id == host.id } && filter.invoke(host)
|
dialog.setFilter { node -> jumpHosts.none { it.id == node.host.id } && filter.invoke(node.host) }
|
||||||
}
|
dialog.setTreeName("HostOptionsPane.JumpHostsOption.Tree")
|
||||||
|
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val hosts = dialog.hosts
|
val hosts = dialog.hosts
|
||||||
if (hosts.isEmpty()) {
|
if (hosts.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts.forEach {
|
hosts.forEach {
|
||||||
val rowCount = model.rowCount
|
val rowCount = model.rowCount
|
||||||
jumpHosts.add(it)
|
jumpHosts.add(it)
|
||||||
|
|||||||
@@ -1,660 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.actions.NewHostAction
|
|
||||||
import app.termora.actions.OpenHostAction
|
|
||||||
import app.termora.transport.SFTPAction
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
|
||||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
|
||||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.datatransfer.DataFlavor
|
|
||||||
import java.awt.datatransfer.Transferable
|
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.event.CellEditorListener
|
|
||||||
import javax.swing.event.ChangeEvent
|
|
||||||
import javax.swing.event.PopupMenuEvent
|
|
||||||
import javax.swing.event.PopupMenuListener
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
import javax.swing.tree.TreeSelectionModel
|
|
||||||
|
|
||||||
|
|
||||||
class HostTree : JTree(), Disposable {
|
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val editor = OutlineTextField(64)
|
|
||||||
|
|
||||||
var contextmenu = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双击是否打开连接
|
|
||||||
*/
|
|
||||||
var doubleClickConnection = true
|
|
||||||
|
|
||||||
val model = HostTreeModel()
|
|
||||||
val searchableModel = SearchableHostTreeModel(model)
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
setModel(model)
|
|
||||||
isEditable = true
|
|
||||||
dropMode = DropMode.ON_OR_INSERT
|
|
||||||
dragEnabled = true
|
|
||||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
|
||||||
editor.preferredSize = Dimension(220, 0)
|
|
||||||
|
|
||||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
|
||||||
override fun getTreeCellRendererComponent(
|
|
||||||
tree: JTree,
|
|
||||||
value: Any,
|
|
||||||
sel: Boolean,
|
|
||||||
expanded: Boolean,
|
|
||||||
leaf: Boolean,
|
|
||||||
row: Int,
|
|
||||||
hasFocus: Boolean
|
|
||||||
): Component {
|
|
||||||
val host = value as Host
|
|
||||||
var text = host.name
|
|
||||||
|
|
||||||
// 是否显示更多信息
|
|
||||||
if (properties.getString("HostTree.showMoreInfo", "false").toBoolean()) {
|
|
||||||
val color = if (sel) {
|
|
||||||
if (this@HostTree.hasFocus()) {
|
|
||||||
UIManager.getColor("textHighlightText")
|
|
||||||
} else {
|
|
||||||
this.foreground
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UIManager.getColor("textInactiveText")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.protocol == Protocol.SSH) {
|
|
||||||
text = """
|
|
||||||
<html>${host.name}
|
|
||||||
|
|
||||||
<font color=rgb(${color.red},${color.green},${color.blue})>${host.username}@${host.host}</font></html>
|
|
||||||
""".trimIndent()
|
|
||||||
} else if (host.protocol == Protocol.Serial) {
|
|
||||||
text = """
|
|
||||||
<html>${host.name}
|
|
||||||
|
|
||||||
<font color=rgb(${color.red},${color.green},${color.blue})>${host.options.serialComm.port}</font></html>
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
|
||||||
|
|
||||||
icon = when (host.protocol) {
|
|
||||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
|
||||||
Protocol.Serial -> if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
|
||||||
else -> if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setCellEditor(object : DefaultCellEditor(editor) {
|
|
||||||
override fun isCellEditable(e: EventObject?): Boolean {
|
|
||||||
if (e is MouseEvent) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return super.isCellEditable(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
|
|
||||||
if (state != null) {
|
|
||||||
TreeUtils.loadExpansionState(this@HostTree, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertValueToText(
|
|
||||||
value: Any?,
|
|
||||||
selected: Boolean,
|
|
||||||
expanded: Boolean,
|
|
||||||
leaf: Boolean,
|
|
||||||
row: Int,
|
|
||||||
hasFocus: Boolean
|
|
||||||
): String {
|
|
||||||
if (value is Host) {
|
|
||||||
return value.name
|
|
||||||
}
|
|
||||||
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
// 右键选中
|
|
||||||
addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestFocusInWindow()
|
|
||||||
|
|
||||||
val selectionRows = selectionModel.selectionRows
|
|
||||||
|
|
||||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
|
||||||
if (selRow < 0) {
|
|
||||||
selectionModel.clearSelection()
|
|
||||||
return
|
|
||||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionPath = getPathForLocation(e.x, e.y)
|
|
||||||
|
|
||||||
setSelectionRow(selRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
|
||||||
val host = lastSelectedPathComponent
|
|
||||||
if (host is Host && host.protocol != Protocol.Folder) {
|
|
||||||
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// contextmenu
|
|
||||||
addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SwingUtilities.invokeLater { showContextMenu(e) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// rename
|
|
||||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
|
||||||
override fun editingStopped(e: ChangeEvent) {
|
|
||||||
val lastHost = lastSelectedPathComponent
|
|
||||||
if (lastHost !is Host || editor.text.isBlank() || editor.text == lastHost.name) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runCatchingHost(lastHost.copy(name = editor.text))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun editingCanceled(e: ChangeEvent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// drag
|
|
||||||
transferHandler = object : TransferHandler() {
|
|
||||||
|
|
||||||
override fun createTransferable(c: JComponent): Transferable {
|
|
||||||
val nodes = selectionModel.selectionPaths
|
|
||||||
.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
val iterator = nodes.iterator()
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val node = iterator.next()
|
|
||||||
val parents = model.getPathToRoot(node).filter { it != node }
|
|
||||||
if (parents.any { nodes.contains(it) }) {
|
|
||||||
iterator.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MoveHostTransferable(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSourceActions(c: JComponent?): Int {
|
|
||||||
return MOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canImport(support: TransferSupport): Boolean {
|
|
||||||
if (!support.isDrop) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val dropLocation = support.dropLocation
|
|
||||||
if (dropLocation !is JTree.DropLocation || support.component != this@HostTree
|
|
||||||
|| dropLocation.childIndex != -1
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastNode = dropLocation.path.lastPathComponent
|
|
||||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
|
||||||
val nodes = support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>
|
|
||||||
if (nodes.any { it == lastNode }) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (parent in model.getPathToRoot(lastNode).filter { it != lastNode }) {
|
|
||||||
if (nodes.any { it == parent }) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
support.setShowDropLocation(true)
|
|
||||||
return support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun importData(support: TransferSupport): Boolean {
|
|
||||||
if (!support.isDrop) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val dropLocation = support.dropLocation
|
|
||||||
if (dropLocation !is JTree.DropLocation) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastNode = dropLocation.path.lastPathComponent
|
|
||||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val hosts = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>)
|
|
||||||
.filterIsInstance<Host>().toMutableList()
|
|
||||||
if (hosts.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录展开的节点
|
|
||||||
val expandedHosts = mutableListOf<String>()
|
|
||||||
for (host in hosts) {
|
|
||||||
model.visit(host) {
|
|
||||||
if (it.protocol == Protocol.Folder) {
|
|
||||||
if (isExpanded(TreePath(model.getPathToRoot(it)))) {
|
|
||||||
expandedHosts.addFirst(it.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = System.currentTimeMillis()
|
|
||||||
for (host in hosts) {
|
|
||||||
model.removeNodeFromParent(host)
|
|
||||||
val newHost = host.copy(
|
|
||||||
parentId = lastNode.id,
|
|
||||||
sort = ++now,
|
|
||||||
updateDate = now
|
|
||||||
)
|
|
||||||
runCatchingHost(newHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
expandNode(lastNode)
|
|
||||||
|
|
||||||
// 展开
|
|
||||||
for (id in expandedHosts) {
|
|
||||||
model.getHost(id)?.let { expandNode(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isPathEditable(path: TreePath?): Boolean {
|
|
||||||
if (path == null) return false
|
|
||||||
if (path.lastPathComponent == model.root) return false
|
|
||||||
return super.isPathEditable(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLastSelectedPathComponent(): Any? {
|
|
||||||
val last = super.getLastSelectedPathComponent() ?: return null
|
|
||||||
if (last is Host) {
|
|
||||||
return model.getHost(last.id) ?: last
|
|
||||||
}
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showContextMenu(event: MouseEvent) {
|
|
||||||
if (!contextmenu) return
|
|
||||||
|
|
||||||
val lastHost = lastSelectedPathComponent
|
|
||||||
if (lastHost !is Host) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val properties = Database.getDatabase().properties
|
|
||||||
val popupMenu = FlatPopupMenu()
|
|
||||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
|
||||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
|
||||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
|
||||||
|
|
||||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
|
||||||
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
|
||||||
val openWithSFTP = openWith.add("SFTP")
|
|
||||||
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
|
||||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
|
||||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
|
||||||
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
|
||||||
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
popupMenu.add(newMenu)
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
|
|
||||||
val showMoreInfo = JCheckBoxMenuItem(I18n.getString("termora.welcome.contextmenu.show-more-info"))
|
|
||||||
showMoreInfo.isSelected = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
|
||||||
showMoreInfo.addActionListener {
|
|
||||||
properties.putString(
|
|
||||||
"HostTree.showMoreInfo",
|
|
||||||
showMoreInfo.isSelected.toString()
|
|
||||||
)
|
|
||||||
SwingUtilities.updateComponentTreeUI(this)
|
|
||||||
}
|
|
||||||
popupMenu.add(showMoreInfo)
|
|
||||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
|
||||||
|
|
||||||
open.addActionListener { openHosts(it, false) }
|
|
||||||
openWithSFTP.addActionListener { openWithSFTP(it) }
|
|
||||||
openWithSFTPCommand.addActionListener { openWithSFTPCommand(it) }
|
|
||||||
openInNewWindow.addActionListener { openHosts(it, true) }
|
|
||||||
|
|
||||||
// 如果选中了 SSH 服务器,那么才启用
|
|
||||||
openWithSFTP.isEnabled = getSelectionNodes().any { it.protocol == Protocol.SSH }
|
|
||||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
|
||||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
|
||||||
|
|
||||||
rename.addActionListener {
|
|
||||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
|
||||||
}
|
|
||||||
|
|
||||||
expandAll.addActionListener {
|
|
||||||
getSelectionNodes().forEach { expandNode(it, true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
colspanAll.addActionListener {
|
|
||||||
selectionModel.selectionPaths.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
.filter { it.protocol == Protocol.Folder }
|
|
||||||
.forEach { collapseNode(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
copy.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
val parent = model.getParent(lastHost) ?: return
|
|
||||||
val node = copyNode(parent, lastHost)
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(node))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
remove.addActionListener {
|
|
||||||
if (OptionPane.showConfirmDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(this),
|
|
||||||
I18n.getString("termora.keymgr.delete-warning"),
|
|
||||||
I18n.getString("termora.remove"),
|
|
||||||
JOptionPane.YES_NO_OPTION,
|
|
||||||
JOptionPane.QUESTION_MESSAGE
|
|
||||||
) == JOptionPane.YES_OPTION
|
|
||||||
) {
|
|
||||||
var lastParent: Host? = null
|
|
||||||
while (!selectionModel.isSelectionEmpty) {
|
|
||||||
val host = lastSelectedPathComponent ?: break
|
|
||||||
if (host !is Host) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
lastParent = model.getParent(host)
|
|
||||||
}
|
|
||||||
model.visit(host) { hostManager.removeHost(it.id) }
|
|
||||||
}
|
|
||||||
if (lastParent != null) {
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(lastParent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newFolder.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
if (lastHost.protocol != Protocol.Folder) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val host = Host(
|
|
||||||
id = UUID.randomUUID().toSimpleString(),
|
|
||||||
protocol = Protocol.Folder,
|
|
||||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
|
||||||
sort = System.currentTimeMillis(),
|
|
||||||
parentId = lastHost.id
|
|
||||||
)
|
|
||||||
|
|
||||||
runCatchingHost(host)
|
|
||||||
|
|
||||||
expandNode(lastHost)
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(host))
|
|
||||||
startEditingAtPath(selectionPath)
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
newHost.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
|
|
||||||
?.actionPerformed(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
property.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
|
||||||
dialog.title = lastHost.name
|
|
||||||
dialog.isVisible = true
|
|
||||||
val host = dialog.host ?: return
|
|
||||||
runCatchingHost(host)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化状态
|
|
||||||
newFolder.isEnabled = lastHost.protocol == Protocol.Folder
|
|
||||||
newHost.isEnabled = newFolder.isEnabled
|
|
||||||
remove.isEnabled = !getSelectionNodes().any { it == model.root }
|
|
||||||
copy.isEnabled = remove.isEnabled
|
|
||||||
rename.isEnabled = remove.isEnabled
|
|
||||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
|
||||||
|
|
||||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
|
||||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
|
||||||
this@HostTree.grabFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
|
||||||
this@HostTree.requestFocusInWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
popupMenu.show(this, event.x, event.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
|
||||||
if (nodes.isEmpty()) return
|
|
||||||
val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
|
||||||
val source = if (openInNewWindow)
|
|
||||||
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
|
||||||
else evt.source
|
|
||||||
|
|
||||||
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openWithSFTP(evt: EventObject) {
|
|
||||||
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
|
||||||
if (nodes.isEmpty()) return
|
|
||||||
|
|
||||||
val sftpAction = ActionManager.getInstance().getAction(Actions.SFTP) as SFTPAction? ?: return
|
|
||||||
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
|
||||||
for (node in nodes) {
|
|
||||||
sftpAction.connectHost(node, tab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openWithSFTPCommand(evt: EventObject) {
|
|
||||||
val nodes = getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
|
||||||
if (nodes.isEmpty()) return
|
|
||||||
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
|
||||||
for (host in nodes) {
|
|
||||||
action.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun expandNode(node: Host, including: Boolean = false) {
|
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
|
||||||
if (including) {
|
|
||||||
model.getChildren(node).forEach { expandNode(it, true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun copyNode(
|
|
||||||
parent: Host,
|
|
||||||
host: Host,
|
|
||||||
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() }
|
|
||||||
): Host {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val newHost = host.copy(
|
|
||||||
name = "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}",
|
|
||||||
id = idGenerator.invoke(),
|
|
||||||
parentId = parent.id,
|
|
||||||
updateDate = now,
|
|
||||||
createDate = now,
|
|
||||||
sort = now
|
|
||||||
)
|
|
||||||
|
|
||||||
runCatchingHost(newHost)
|
|
||||||
|
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
for (child in model.getChildren(host)) {
|
|
||||||
copyNode(newHost, child, idGenerator)
|
|
||||||
}
|
|
||||||
if (isExpanded(TreePath(model.getPathToRoot(host)))) {
|
|
||||||
expandNode(newHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newHost
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCatchingHost(host: Host) {
|
|
||||||
hostManager.addHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collapseNode(node: Host) {
|
|
||||||
model.getChildren(node).forEach { collapseNode(it) }
|
|
||||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSelectionNodes(): List<Host> {
|
|
||||||
val selectionNodes = selectionModel.selectionPaths.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
|
|
||||||
if (selectionNodes.isEmpty()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodes = mutableListOf<Host>()
|
|
||||||
val parents = mutableListOf<Host>()
|
|
||||||
|
|
||||||
for (node in selectionNodes) {
|
|
||||||
if (node.protocol == Protocol.Folder) {
|
|
||||||
parents.add(node)
|
|
||||||
}
|
|
||||||
nodes.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parents.isNotEmpty()) {
|
|
||||||
val p = parents.removeFirst()
|
|
||||||
for (i in 0 until getModel().getChildCount(p)) {
|
|
||||||
val child = getModel().getChild(p, i) as Host
|
|
||||||
nodes.add(child)
|
|
||||||
parents.add(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保是最新的
|
|
||||||
for (i in 0 until nodes.size) {
|
|
||||||
nodes[i] = model.getHost(nodes[i].id) ?: continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
Database.getDatabase().properties.putString(
|
|
||||||
"HostTreeExpansionState",
|
|
||||||
TreeUtils.saveExpansionState(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class HostTreeNodeTransferable(val hosts: List<Host>) :
|
|
||||||
Transferable {
|
|
||||||
|
|
||||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
|
||||||
return arrayOf(getDataFlavor())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDataFlavorSupported(flavor: DataFlavor): Boolean {
|
|
||||||
return getDataFlavor() == flavor
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTransferData(flavor: DataFlavor): Any {
|
|
||||||
return hosts
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getDataFlavor(): DataFlavor
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MoveHostTransferable(hosts: List<Host>) : HostTreeNodeTransferable(hosts) {
|
|
||||||
companion object {
|
|
||||||
val dataFlavor =
|
|
||||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataFlavor(): DataFlavor {
|
|
||||||
return dataFlavor
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.tree.TreeSelectionModel
|
|
||||||
|
|
||||||
class HostTreeDialog(
|
|
||||||
owner: Window,
|
|
||||||
private val filter: (host: Host) -> Boolean = { true }
|
|
||||||
) : DialogWrapper(owner) {
|
|
||||||
|
|
||||||
private val tree = HostTree()
|
|
||||||
|
|
||||||
val hosts = mutableListOf<Host>()
|
|
||||||
|
|
||||||
var allowMulti = true
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value) {
|
|
||||||
tree.selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
|
||||||
} else {
|
|
||||||
tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
|
||||||
isModal = true
|
|
||||||
isResizable = false
|
|
||||||
controlsVisible = false
|
|
||||||
title = I18n.getString("termora.transport.sftp.select-host")
|
|
||||||
|
|
||||||
tree.setModel(SearchableHostTreeModel(tree.model) { host ->
|
|
||||||
(host.protocol == Protocol.Folder || host.protocol == Protocol.SSH) && filter.invoke(host)
|
|
||||||
})
|
|
||||||
tree.contextmenu = true
|
|
||||||
tree.doubleClickConnection = false
|
|
||||||
tree.dragEnabled = false
|
|
||||||
|
|
||||||
initEvents()
|
|
||||||
|
|
||||||
init()
|
|
||||||
setLocationRelativeTo(null)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowActivated(e: WindowEvent) {
|
|
||||||
removeWindowListener(this)
|
|
||||||
val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
|
|
||||||
if (state != null) {
|
|
||||||
TreeUtils.loadExpansionState(tree, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
tree.addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
|
||||||
val node = tree.lastSelectedPathComponent ?: return
|
|
||||||
if (node is Host && node.protocol != Protocol.Folder) {
|
|
||||||
doOKAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
tree.setModel(null)
|
|
||||||
Database.getDatabase().properties.putString(
|
|
||||||
"HostTreeDialog.HostTreeExpansionState",
|
|
||||||
TreeUtils.saveExpansionState(tree)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
|
||||||
val scrollPane = JScrollPane(tree)
|
|
||||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
|
||||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
|
||||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
|
||||||
)
|
|
||||||
|
|
||||||
return scrollPane
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doOKAction() {
|
|
||||||
|
|
||||||
if (allowMulti) {
|
|
||||||
val nodes = tree.getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
|
||||||
if (nodes.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hosts.clear()
|
|
||||||
hosts.addAll(nodes)
|
|
||||||
} else {
|
|
||||||
val node = tree.lastSelectedPathComponent ?: return
|
|
||||||
if (node !is Host || node.protocol != Protocol.SSH) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hosts.clear()
|
|
||||||
hosts.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
super.doOKAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
|
||||||
hosts.clear()
|
|
||||||
super.doCancelAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import javax.swing.event.TreeModelEvent
|
|
||||||
import javax.swing.event.TreeModelListener
|
|
||||||
import javax.swing.tree.TreeModel
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
|
|
||||||
class HostTreeModel : TreeModel {
|
|
||||||
|
|
||||||
val listeners = mutableListOf<TreeModelListener>()
|
|
||||||
|
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val hosts = mutableMapOf<String, Host>()
|
|
||||||
private val myRoot by lazy {
|
|
||||||
Host(
|
|
||||||
id = "0",
|
|
||||||
protocol = Protocol.Folder,
|
|
||||||
name = I18n.getString("termora.welcome.my-hosts"),
|
|
||||||
host = StringUtils.EMPTY,
|
|
||||||
port = 0,
|
|
||||||
remark = StringUtils.EMPTY,
|
|
||||||
username = StringUtils.EMPTY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
hosts[host.id] = host
|
|
||||||
}
|
|
||||||
|
|
||||||
hostManager.addHostListener(object : HostListener {
|
|
||||||
override fun hostRemoved(id: String) {
|
|
||||||
val host = hosts[id] ?: return
|
|
||||||
removeNodeFromParent(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hostAdded(host: Host) {
|
|
||||||
// 如果已经存在,那么是修改
|
|
||||||
if (hosts.containsKey(host.id)) {
|
|
||||||
val oldHost = hosts.getValue(host.id)
|
|
||||||
// 父级结构变了
|
|
||||||
if (oldHost.parentId != host.parentId) {
|
|
||||||
hostRemoved(host.id)
|
|
||||||
hostAdded(host)
|
|
||||||
} else {
|
|
||||||
hosts[host.id] = host
|
|
||||||
val event = TreeModelEvent(this, getPathToRoot(host))
|
|
||||||
listeners.forEach { it.treeStructureChanged(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
hosts[host.id] = host
|
|
||||||
val parent = getParent(host) ?: return
|
|
||||||
val path = TreePath(getPathToRoot(parent))
|
|
||||||
val event = TreeModelEvent(this, path, intArrayOf(getIndexOfChild(parent, host)), arrayOf(host))
|
|
||||||
listeners.forEach { it.treeNodesInserted(event) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hostsChanged() {
|
|
||||||
hosts.clear()
|
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
hosts[host.id] = host
|
|
||||||
}
|
|
||||||
val event = TreeModelEvent(this, getPathToRoot(root), null, null)
|
|
||||||
listeners.forEach { it.treeStructureChanged(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRoot(): Host {
|
|
||||||
return myRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChild(parent: Any?, index: Int): Any {
|
|
||||||
return getChildren(parent)[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChildCount(parent: Any?): Int {
|
|
||||||
return getChildren(parent).size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isLeaf(node: Any?): Boolean {
|
|
||||||
return getChildCount(node) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getParent(node: Host): Host? {
|
|
||||||
if (node.parentId == root.id || root.id == node.id) {
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
return hosts.values.firstOrNull { it.id == node.parentId }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
|
||||||
return getChildren(parent).indexOf(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTreeModelListener(listener: TreeModelListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeTreeModelListener(listener: TreeModelListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 仅从结构中删除
|
|
||||||
*/
|
|
||||||
fun removeNodeFromParent(host: Host) {
|
|
||||||
val parent = getParent(host) ?: return
|
|
||||||
val index = getIndexOfChild(parent, host)
|
|
||||||
val event = TreeModelEvent(this, TreePath(getPathToRoot(parent)), intArrayOf(index), arrayOf(host))
|
|
||||||
hosts.remove(host.id)
|
|
||||||
listeners.forEach { it.treeNodesRemoved(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun visit(host: Host, visitor: (host: Host) -> Unit) {
|
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
getChildren(host).forEach { visit(it, visitor) }
|
|
||||||
visitor.invoke(host)
|
|
||||||
} else {
|
|
||||||
visitor.invoke(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHost(id: String): Host? {
|
|
||||||
return hosts[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPathToRoot(host: Host): Array<Host> {
|
|
||||||
|
|
||||||
if (host.id == root.id) {
|
|
||||||
return arrayOf(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
val parents = mutableListOf(host)
|
|
||||||
var pId = host.parentId
|
|
||||||
while (pId != root.id) {
|
|
||||||
val e = hosts[(pId)] ?: break
|
|
||||||
parents.addFirst(e)
|
|
||||||
pId = e.parentId
|
|
||||||
}
|
|
||||||
parents.addFirst(root)
|
|
||||||
return parents.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChildren(parent: Any?): List<Host> {
|
|
||||||
val pId = if (parent is Host) parent.id else root.id
|
|
||||||
return hosts.values.filter { it.parentId == pId }
|
|
||||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
src/main/kotlin/app/termora/HostTreeNode.kt
Normal file
48
src/main/kotlin/app/termora/HostTreeNode.kt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode
|
||||||
|
|
||||||
|
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||||
|
var host: Host
|
||||||
|
get() = userObject as Host
|
||||||
|
set(value) = setUserObject(value)
|
||||||
|
|
||||||
|
val folderCount
|
||||||
|
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
|
||||||
|
|
||||||
|
override fun getParent(): HostTreeNode? {
|
||||||
|
return super.getParent() as HostTreeNode?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllChildren(): List<HostTreeNode> {
|
||||||
|
val children = mutableListOf<HostTreeNode>()
|
||||||
|
for (child in children()) {
|
||||||
|
if (child is HostTreeNode) {
|
||||||
|
children.add(child)
|
||||||
|
children.addAll(child.getAllChildren())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun clone(): Any {
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
newNode.children = null
|
||||||
|
newNode.parent = null
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as HostTreeNode
|
||||||
|
|
||||||
|
return host == other.host
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return host.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
612
src/main/kotlin/app/termora/NewHostTree.kt
Normal file
612
src/main/kotlin/app/termora/NewHostTree.kt
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.OpenHostAction
|
||||||
|
import app.termora.transport.SFTPAction
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.JXTree
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.datatransfer.Transferable
|
||||||
|
import java.awt.datatransfer.UnsupportedFlavorException
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.ActionListener
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.util.*
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.CellEditorListener
|
||||||
|
import javax.swing.event.ChangeEvent
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
|
import javax.swing.tree.TreeSelectionModel
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class NewHostTree : JXTree() {
|
||||||
|
|
||||||
|
private val tree = this
|
||||||
|
private val editor = OutlineTextField(64)
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
private var isShowMoreInfo
|
||||||
|
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
|
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||||
|
|
||||||
|
private val model = NewHostTreeModel()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许显示右键菜单
|
||||||
|
*/
|
||||||
|
var contextmenu = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许双击连接
|
||||||
|
*/
|
||||||
|
var doubleClickConnection = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
initViews()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initViews() {
|
||||||
|
super.setModel(model)
|
||||||
|
isEditable = true
|
||||||
|
dragEnabled = true
|
||||||
|
isRootVisible = true
|
||||||
|
dropMode = DropMode.ON_OR_INSERT
|
||||||
|
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||||
|
editor.preferredSize = Dimension(220, 0)
|
||||||
|
|
||||||
|
// renderer
|
||||||
|
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||||
|
override fun getTreeCellRendererComponent(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val node = value as HostTreeNode
|
||||||
|
val host = node.host
|
||||||
|
var text = host.name
|
||||||
|
|
||||||
|
// 是否显示更多信息
|
||||||
|
if (isShowMoreInfo) {
|
||||||
|
val color = if (sel) {
|
||||||
|
if (tree.hasFocus()) {
|
||||||
|
UIManager.getColor("textHighlightText")
|
||||||
|
} else {
|
||||||
|
this.foreground
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontTag = Function<String, String> {
|
||||||
|
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
text =
|
||||||
|
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
text =
|
||||||
|
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||||
|
} else if (host.protocol == Protocol.Folder) {
|
||||||
|
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||||
|
|
||||||
|
icon = when (host.protocol) {
|
||||||
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
|
Protocol.Serial -> if (sel && tree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||||
|
else -> if (sel && tree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// rename
|
||||||
|
setCellEditor(object : DefaultCellEditor(editor) {
|
||||||
|
override fun isCellEditable(e: EventObject?): Boolean {
|
||||||
|
if (e is MouseEvent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return super.isCellEditable(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCellEditorValue(): Any {
|
||||||
|
val node = lastSelectedPathComponent as HostTreeNode
|
||||||
|
return node.host
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
// 右键选中
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFocusInWindow()
|
||||||
|
|
||||||
|
val selectionRows = selectionModel.selectionRows
|
||||||
|
|
||||||
|
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||||
|
if (selRow < 0) {
|
||||||
|
selectionModel.clearSelection()
|
||||||
|
return
|
||||||
|
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionPath = getPathForLocation(e.x, e.y)
|
||||||
|
|
||||||
|
setSelectionRow(selRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// contextmenu
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextmenu) {
|
||||||
|
SwingUtilities.invokeLater { showContextmenu(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||||
|
if (lastNode.host.protocol != Protocol.Folder) {
|
||||||
|
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// rename
|
||||||
|
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||||
|
override fun editingStopped(e: ChangeEvent) {
|
||||||
|
val lastHost = lastSelectedPathComponent
|
||||||
|
if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastHost.host = lastHost.host.copy(name = editor.text)
|
||||||
|
hostManager.addHost(lastHost.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun editingCanceled(e: ChangeEvent) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// drag
|
||||||
|
transferHandler = object : TransferHandler() {
|
||||||
|
|
||||||
|
override fun createTransferable(c: JComponent): Transferable? {
|
||||||
|
val nodes = getSelectionHostTreeNodes().toMutableList()
|
||||||
|
if (nodes.isEmpty()) return null
|
||||||
|
if (nodes.contains(model.root)) return null
|
||||||
|
|
||||||
|
val iterator = nodes.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val node = iterator.next()
|
||||||
|
val parents = model.getPathToRoot(node).filter { it != node }
|
||||||
|
if (parents.any { nodes.contains(it) }) {
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MoveHostTransferable(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceActions(c: JComponent?): Int {
|
||||||
|
return MOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canImport(support: TransferSupport): Boolean {
|
||||||
|
if (support.component != tree) return false
|
||||||
|
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||||
|
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||||
|
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) return false
|
||||||
|
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||||
|
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||||
|
if (nodes.isEmpty()) return false
|
||||||
|
if (node.host.protocol != Protocol.Folder) return false
|
||||||
|
|
||||||
|
for (e in nodes) {
|
||||||
|
// 禁止拖拽到自己的子下面
|
||||||
|
if (dropLocation.path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(dropLocation.path)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件夹只能拖拽到文件夹的下面
|
||||||
|
if (e.host.protocol == Protocol.Folder) {
|
||||||
|
if (dropLocation.childIndex > node.folderCount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (dropLocation.childIndex != -1) {
|
||||||
|
// 非文件夹也不能拖拽到文件夹的上面
|
||||||
|
if (dropLocation.childIndex < node.folderCount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val p = e.parent ?: continue
|
||||||
|
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||||
|
if (p == node && dropLocation.childIndex != -1) {
|
||||||
|
val idx = p.getIndex(e)
|
||||||
|
if (dropLocation.childIndex in idx..idx + 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
support.setShowDropLocation(true)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun importData(support: TransferSupport): Boolean {
|
||||||
|
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||||
|
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||||
|
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||||
|
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||||
|
|
||||||
|
// 展开的 host id
|
||||||
|
val expanded = mutableSetOf(node.host.id)
|
||||||
|
for (e in nodes) {
|
||||||
|
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||||
|
.map { it.host.id }.forEach { expanded.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转移
|
||||||
|
for (e in nodes) {
|
||||||
|
model.removeNodeFromParent(e)
|
||||||
|
e.host = e.host.copy(parentId = node.host.id)
|
||||||
|
hostManager.addHost(e.host)
|
||||||
|
|
||||||
|
if (dropLocation.childIndex == -1) {
|
||||||
|
if (e.host.protocol == Protocol.Folder) {
|
||||||
|
model.insertNodeInto(e, node, node.folderCount)
|
||||||
|
} else {
|
||||||
|
model.insertNodeInto(e, node, node.childCount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (e.host.protocol == Protocol.Folder) {
|
||||||
|
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||||
|
} else {
|
||||||
|
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先展开最顶级的
|
||||||
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
|
||||||
|
for (child in node.getAllChildren()) {
|
||||||
|
if (expanded.contains(child.host.id)) {
|
||||||
|
expandPath(TreePath(model.getPathToRoot(child)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun showContextmenu(event: MouseEvent) {
|
||||||
|
val lastNode = lastSelectedPathComponent
|
||||||
|
if (lastNode !is HostTreeNode) return
|
||||||
|
|
||||||
|
val lastNodeParent = lastNode.parent ?: model.root
|
||||||
|
val lastHost = lastNode.host
|
||||||
|
|
||||||
|
val popupMenu = FlatPopupMenu()
|
||||||
|
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||||
|
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||||
|
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||||
|
|
||||||
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||||
|
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||||
|
val openWithSFTP = openWith.add("SFTP")
|
||||||
|
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||||
|
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||||
|
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val refresh = popupMenu.add(I18n.getString("termora.welcome.contextmenu.refresh"))
|
||||||
|
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
||||||
|
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
popupMenu.add(newMenu)
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val showMoreInfo = JCheckBoxMenuItem(I18n.getString("termora.welcome.contextmenu.show-more-info"))
|
||||||
|
showMoreInfo.isSelected = isShowMoreInfo
|
||||||
|
showMoreInfo.addActionListener {
|
||||||
|
isShowMoreInfo = !isShowMoreInfo
|
||||||
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
|
}
|
||||||
|
popupMenu.add(showMoreInfo)
|
||||||
|
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
|
open.addActionListener { openHosts(it, false) }
|
||||||
|
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||||
|
openWithSFTP.addActionListener { openWithSFTP(it) }
|
||||||
|
openWithSFTPCommand.addActionListener { openWithSFTPCommand(it) }
|
||||||
|
newFolder.addActionListener {
|
||||||
|
val host = Host(
|
||||||
|
id = UUID.randomUUID().toSimpleString(),
|
||||||
|
protocol = Protocol.Folder,
|
||||||
|
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
parentId = lastHost.id
|
||||||
|
)
|
||||||
|
hostManager.addHost(host)
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.folderCount)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
startEditingAtPath(selectionPath)
|
||||||
|
}
|
||||||
|
remove.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val nodes = getSelectionHostTreeNodes()
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(tree),
|
||||||
|
I18n.getString("termora.keymgr.delete-warning"),
|
||||||
|
I18n.getString("termora.remove"),
|
||||||
|
JOptionPane.YES_NO_OPTION,
|
||||||
|
JOptionPane.QUESTION_MESSAGE
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
for (c in nodes) {
|
||||||
|
hostManager.addHost(c.host.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
||||||
|
model.removeNodeFromParent(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
copy.addActionListener {
|
||||||
|
for (c in getSelectionHostTreeNodes()) {
|
||||||
|
val p = c.parent ?: continue
|
||||||
|
val newNode = copyNode(c, p.host.id)
|
||||||
|
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||||
|
expandAll.addActionListener {
|
||||||
|
for (node in getSelectionHostTreeNodes(true)) {
|
||||||
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colspanAll.addActionListener {
|
||||||
|
for (node in getSelectionHostTreeNodes(true).reversed()) {
|
||||||
|
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newHost.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = HostDialog(owner)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
hostManager.addHost(host)
|
||||||
|
val c = HostTreeNode(host)
|
||||||
|
val newNode = copyNode(c, lastHost.id)
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
property.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = HostDialog(owner, lastHost)
|
||||||
|
dialog.title = lastHost.name
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = dialog.host ?: return
|
||||||
|
lastNode.host = host
|
||||||
|
hostManager.addHost(host)
|
||||||
|
model.nodeStructureChanged(lastNode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
refresh.addActionListener {
|
||||||
|
val expanded = mutableSetOf(lastNode.host.id)
|
||||||
|
for (e in lastNode.getAllChildren()) {
|
||||||
|
if (e.host.protocol == Protocol.Folder && isExpanded(TreePath(model.getPathToRoot(e)))) {
|
||||||
|
expanded.add(e.host.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
model.reload(lastNode)
|
||||||
|
|
||||||
|
// 先展开最顶级的
|
||||||
|
expandPath(TreePath(model.getPathToRoot(lastNode)))
|
||||||
|
|
||||||
|
for (child in lastNode.getAllChildren()) {
|
||||||
|
if (expanded.contains(child.host.id)) {
|
||||||
|
expandPath(TreePath(model.getPathToRoot(child)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newFolder.isEnabled = lastHost.protocol == Protocol.Folder
|
||||||
|
newHost.isEnabled = newFolder.isEnabled
|
||||||
|
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root }
|
||||||
|
copy.isEnabled = remove.isEnabled
|
||||||
|
rename.isEnabled = remove.isEnabled
|
||||||
|
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||||
|
refresh.isEnabled = lastHost.protocol == Protocol.Folder
|
||||||
|
|
||||||
|
|
||||||
|
// 如果选中了 SSH 服务器,那么才启用
|
||||||
|
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH }
|
||||||
|
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||||
|
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||||
|
|
||||||
|
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
|
tree.grabFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||||
|
tree.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
popupMenu.show(this, event.x, event.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyNode(
|
||||||
|
node: HostTreeNode,
|
||||||
|
parentId: String,
|
||||||
|
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() },
|
||||||
|
level: Int = 0
|
||||||
|
): HostTreeNode {
|
||||||
|
|
||||||
|
val host = node.host
|
||||||
|
val now = host.sort + 1
|
||||||
|
val name = if (level == 0) "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}"
|
||||||
|
else host.name
|
||||||
|
|
||||||
|
val newHost = host.copy(
|
||||||
|
id = idGenerator.invoke(),
|
||||||
|
name = name,
|
||||||
|
parentId = parentId,
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
createDate = System.currentTimeMillis(),
|
||||||
|
sort = now
|
||||||
|
)
|
||||||
|
val newNode = HostTreeNode(newHost)
|
||||||
|
|
||||||
|
hostManager.addHost(newHost)
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.Folder) {
|
||||||
|
for (child in node.children()) {
|
||||||
|
if (child is HostTreeNode) {
|
||||||
|
newNode.add(copyNode(child, newHost.id, idGenerator, level + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包含孙子
|
||||||
|
*/
|
||||||
|
fun getSelectionHostTreeNodes(include: Boolean = false): List<HostTreeNode> {
|
||||||
|
val paths = selectionPaths ?: return emptyList()
|
||||||
|
if (paths.isEmpty()) return emptyList()
|
||||||
|
val nodes = mutableListOf<HostTreeNode>()
|
||||||
|
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||||
|
.filterIsInstance<HostTreeNode>().toMutableList()
|
||||||
|
|
||||||
|
if (include) {
|
||||||
|
while (parents.isNotEmpty()) {
|
||||||
|
val node = parents.removeFirst()
|
||||||
|
nodes.add(node)
|
||||||
|
parents.addAll(node.children().toList().filterIsInstance<HostTreeNode>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (include) nodes else parents
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
val source = if (openInNewWindow)
|
||||||
|
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||||
|
else evt.source
|
||||||
|
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openWithSFTP(evt: EventObject) {
|
||||||
|
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
|
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return
|
||||||
|
val tab = sftpAction.openOrCreateSFTPTerminalTab(AnActionEvent(this, StringUtils.EMPTY, evt)) ?: return
|
||||||
|
for (node in nodes) {
|
||||||
|
sftpAction.connectHost(node, tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openWithSFTPCommand(evt: EventObject) {
|
||||||
|
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
for (host in nodes) {
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {
|
||||||
|
companion object {
|
||||||
|
val dataFlavor =
|
||||||
|
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||||
|
return arrayOf(dataFlavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||||
|
return dataFlavor == flavor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||||
|
if (flavor == dataFlavor) {
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
throw UnsupportedFlavorException(flavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
87
src/main/kotlin/app/termora/NewHostTreeDialog.kt
Normal file
87
src/main/kotlin/app/termora/NewHostTreeDialog.kt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JScrollPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
|
class NewHostTreeDialog(
|
||||||
|
owner: Window,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
var hosts = emptyList<Host>()
|
||||||
|
var allowMulti = true
|
||||||
|
|
||||||
|
private var filter: Function<HostTreeNode, Boolean> = Function<HostTreeNode, Boolean> { true }
|
||||||
|
private val tree = NewHostTree()
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||||
|
isModal = true
|
||||||
|
isResizable = false
|
||||||
|
controlsVisible = false
|
||||||
|
title = I18n.getString("termora.transport.sftp.select-host")
|
||||||
|
|
||||||
|
tree.contextmenu = false
|
||||||
|
tree.doubleClickConnection = false
|
||||||
|
tree.dragEnabled = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
init()
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFilter(filter: Function<HostTreeNode, Boolean>) {
|
||||||
|
tree.model = FilterableHostTreeModel(tree) { false }.apply {
|
||||||
|
addFilter(filter)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val scrollPane = JScrollPane(tree)
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
||||||
|
)
|
||||||
|
|
||||||
|
return scrollPane
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
hosts = emptyList()
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
hosts = tree.getSelectionHostTreeNodes(true)
|
||||||
|
.filter { filter.apply(it) }
|
||||||
|
.map { it.host }
|
||||||
|
|
||||||
|
if (hosts.isEmpty()) return
|
||||||
|
if (!allowMulti && hosts.size > 1) return
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTreeName(treeName: String) {
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
private val key = "${treeName}.state"
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
|
init {
|
||||||
|
TreeUtils.loadExpansionState(tree, properties.getString(key, StringUtils.EMPTY))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
properties.putString(key, TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/main/kotlin/app/termora/NewHostTreeModel.kt
Normal file
83
src/main/kotlin/app/termora/NewHostTreeModel.kt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import javax.swing.tree.DefaultTreeModel
|
||||||
|
import javax.swing.tree.MutableTreeNode
|
||||||
|
import javax.swing.tree.TreeNode
|
||||||
|
|
||||||
|
|
||||||
|
class NewHostTreeModel : DefaultTreeModel(
|
||||||
|
HostTreeNode(
|
||||||
|
Host(
|
||||||
|
id = "0",
|
||||||
|
protocol = Protocol.Folder,
|
||||||
|
name = I18n.getString("termora.welcome.my-hosts"),
|
||||||
|
host = StringUtils.EMPTY,
|
||||||
|
port = 0,
|
||||||
|
remark = StringUtils.EMPTY,
|
||||||
|
username = StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
private val Host.isRoot get() = this.parentId == "0" || this.parentId.isBlank()
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
|
||||||
|
init {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getRoot(): HostTreeNode {
|
||||||
|
return super.getRoot() as HostTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun reload(parent: TreeNode) {
|
||||||
|
|
||||||
|
if (parent !is HostTreeNode) {
|
||||||
|
super.reload(parent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.removeAllChildren()
|
||||||
|
|
||||||
|
val hosts = hostManager.hosts()
|
||||||
|
val nodes = linkedMapOf<String, HostTreeNode>()
|
||||||
|
|
||||||
|
// 遍历 Host 列表,构建树节点
|
||||||
|
for (host in hosts) {
|
||||||
|
val node = HostTreeNode(host)
|
||||||
|
nodes[host.id] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
for (host in hosts) {
|
||||||
|
val node = nodes[host.id] ?: continue
|
||||||
|
if (host.isRoot) continue
|
||||||
|
val p = nodes[host.parentId] ?: continue
|
||||||
|
p.add(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((_, v) in nodes.entries) {
|
||||||
|
if (parent.host.id == v.host.parentId) {
|
||||||
|
parent.add(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.reload(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
|
||||||
|
super.insertNodeInto(newChild, parent, index)
|
||||||
|
// 重置所有排序
|
||||||
|
if (parent is HostTreeNode) {
|
||||||
|
for ((i, c) in parent.children().toList().filterIsInstance<HostTreeNode>().withIndex()) {
|
||||||
|
val sort = i.toLong()
|
||||||
|
if (c.host.sort == sort) continue
|
||||||
|
c.host = c.host.copy(sort = sort)
|
||||||
|
hostManager.addHost(c.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import javax.swing.event.TreeModelEvent
|
|
||||||
import javax.swing.event.TreeModelListener
|
|
||||||
import javax.swing.tree.TreeModel
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
|
|
||||||
class SearchableHostTreeModel(
|
|
||||||
private val model: HostTreeModel,
|
|
||||||
private val filter: (host: Host) -> Boolean = { true }
|
|
||||||
) : TreeModel {
|
|
||||||
private var text = String()
|
|
||||||
|
|
||||||
override fun getRoot(): Any {
|
|
||||||
return model.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChild(parent: Any?, index: Int): Any {
|
|
||||||
return getChildren(parent)[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChildCount(parent: Any?): Int {
|
|
||||||
return getChildren(parent).size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isLeaf(node: Any?): Boolean {
|
|
||||||
return model.isLeaf(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
|
||||||
return model.valueForPathChanged(path, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
|
||||||
return getChildren(parent).indexOf(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTreeModelListener(l: TreeModelListener) {
|
|
||||||
model.addTreeModelListener(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
|
||||||
model.removeTreeModelListener(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getChildren(parent: Any?): List<Host> {
|
|
||||||
val children = model.getChildren(parent)
|
|
||||||
if (children.isEmpty()) return emptyList()
|
|
||||||
return children.filter { e ->
|
|
||||||
filter.invoke(e)
|
|
||||||
&& e.name.contains(text, true)
|
|
||||||
|| e.host.contains(text, true)
|
|
||||||
|| TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(text: String) {
|
|
||||||
this.text = text
|
|
||||||
model.listeners.forEach {
|
|
||||||
it.treeStructureChanged(
|
|
||||||
TreeModelEvent(
|
|
||||||
this, TreePath(root),
|
|
||||||
null, null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1595,7 +1595,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
val key = doorman.work(passwordTextField.password)
|
val key = doorman.work(passwordTextField.password)
|
||||||
|
|
||||||
hosts.forEach { hostManager.addHost(it, false) }
|
hosts.forEach { hostManager.addHost(it) }
|
||||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||||
for (e in properties) {
|
for (e in properties) {
|
||||||
for ((k, v) in e.second) {
|
for ((k, v) in e.second) {
|
||||||
|
|||||||
@@ -26,11 +26,15 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
private val searchTextField = FlatTextField()
|
private val searchTextField = FlatTextField()
|
||||||
private val hostTree = HostTree()
|
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 filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
||||||
|
searchTextField.text.isBlank()
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -125,8 +129,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
})
|
})
|
||||||
hostTree.showsRootHandles = true
|
hostTree.showsRootHandles = true
|
||||||
|
|
||||||
Disposer.register(this, hostTree)
|
|
||||||
|
|
||||||
val scrollPane = JScrollPane(hostTree)
|
val scrollPane = JScrollPane(hostTree)
|
||||||
scrollPane.verticalScrollBar.maximumSize = Dimension(0, 0)
|
scrollPane.verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||||
scrollPane.verticalScrollBar.preferredSize = Dimension(0, 0)
|
scrollPane.verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||||
@@ -137,6 +139,11 @@ 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
|
||||||
|
TreeUtils.loadExpansionState(
|
||||||
|
hostTree,
|
||||||
|
properties.getString("Welcome.HostTree.state", StringUtils.EMPTY)
|
||||||
|
)
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
@@ -162,48 +169,49 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
|
FindEverywhereProvider.getFindEverywhereProviders(windowScope).add(object : FindEverywhereProvider {
|
||||||
.add(object : FindEverywhereProvider {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
var filter = hostTreeModel.root.getAllChildren()
|
||||||
var filter = TreeUtils.children(hostTree.model, hostTree.model.root)
|
.map { it.host }
|
||||||
.filterIsInstance<Host>()
|
.filter { it.protocol != Protocol.Folder }
|
||||||
.filter { it.protocol != Protocol.Folder }
|
|
||||||
|
|
||||||
if (pattern.isNotBlank()) {
|
if (pattern.isNotBlank()) {
|
||||||
filter = filter.filter {
|
filter = filter.filter {
|
||||||
if (it.protocol == Protocol.SSH) {
|
if (it.protocol == Protocol.SSH) {
|
||||||
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
||||||
} else {
|
} else {
|
||||||
it.name.contains(pattern, true)
|
it.name.contains(pattern, true)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter.map { HostFindEverywhereResult(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun group(): String {
|
return filter.map { HostFindEverywhereResult(it) }
|
||||||
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun order(): Int {
|
override fun group(): String {
|
||||||
return Integer.MIN_VALUE + 2
|
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
override fun order(): Int {
|
||||||
|
return Integer.MIN_VALUE + 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
filterableHostTreeModel.addFilter {
|
||||||
|
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() {
|
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
private var state = StringUtils.EMPTY
|
|
||||||
override fun changedUpdate(e: DocumentEvent) {
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
val text = searchTextField.text
|
val text = searchTextField.text
|
||||||
if (text.isBlank()) {
|
filterableHostTreeModel.refresh()
|
||||||
hostTree.setModel(hostTree.model)
|
if (text.isNotBlank()) {
|
||||||
TreeUtils.loadExpansionState(hostTree, state)
|
hostTree.expandAll()
|
||||||
state = String()
|
|
||||||
} else {
|
|
||||||
if (state.isBlank()) state = TreeUtils.saveExpansionState(hostTree)
|
|
||||||
hostTree.setModel(hostTree.searchableModel)
|
|
||||||
hostTree.searchableModel.search(text)
|
|
||||||
TreeUtils.expandAll(hostTree)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -251,8 +259,8 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
hostTree.setModel(null)
|
|
||||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||||
|
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||||
@@ -276,12 +284,10 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
override fun getText(isSelected: Boolean): String {
|
override fun getText(isSelected: Boolean): String {
|
||||||
if (showMoreInfo) {
|
if (showMoreInfo) {
|
||||||
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
||||||
val moreInfo = if (host.protocol == Protocol.SSH) {
|
val moreInfo = when (host.protocol) {
|
||||||
"${host.username}@${host.host}"
|
Protocol.SSH -> "${host.username}@${host.host}"
|
||||||
} else if (host.protocol == Protocol.Serial) {
|
Protocol.Serial -> host.options.serialComm.port
|
||||||
host.options.serialComm.port
|
else -> StringUtils.EMPTY
|
||||||
} else {
|
|
||||||
StringUtils.EMPTY
|
|
||||||
}
|
}
|
||||||
if (moreInfo.isNotBlank()) {
|
if (moreInfo.isNotBlank()) {
|
||||||
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ object DataProviders {
|
|||||||
|
|
||||||
|
|
||||||
object Welcome {
|
object Welcome {
|
||||||
val HostTree = DataKey(app.termora.HostTree::class)
|
val HostTree = DataKey(app.termora.NewHostTree::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.Host
|
import app.termora.*
|
||||||
import app.termora.HostDialog
|
|
||||||
import app.termora.HostManager
|
|
||||||
import app.termora.Protocol
|
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
class NewHostAction : AnAction() {
|
class NewHostAction : AnAction() {
|
||||||
@@ -20,27 +17,27 @@ class NewHostAction : AnAction() {
|
|||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||||
val model = tree.model
|
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
||||||
var lastHost = tree.lastSelectedPathComponent ?: model.root
|
if (lastNode.host.protocol != Protocol.Folder) {
|
||||||
if (lastHost !is Host) {
|
lastNode = lastNode.parent ?: return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastHost.protocol != Protocol.Folder) {
|
|
||||||
val p = model.getParent(lastHost) ?: return
|
|
||||||
lastHost = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val lastHost = lastNode.host
|
||||||
val dialog = HostDialog(evt.window)
|
val dialog = HostDialog(evt.window)
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
|
||||||
hostManager.addHost(host)
|
hostManager.addHost(host)
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
|
||||||
tree.expandNode(lastHost)
|
val model = if (tree.model is FilterableHostTreeModel) (tree.model as FilterableHostTreeModel).getModel()
|
||||||
|
else tree.model
|
||||||
|
|
||||||
tree.selectionPath = TreePath(model.getPathToRoot(host))
|
if (model is NewHostTreeModel) {
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
|
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,9 +211,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(this) ?: return
|
val owner = SwingUtilities.getWindowAncestor(this) ?: return
|
||||||
val hostTreeDialog = HostTreeDialog(owner) {
|
val hostTreeDialog = NewHostTreeDialog(owner)
|
||||||
it.protocol == Protocol.SSH
|
hostTreeDialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||||
}
|
hostTreeDialog.setTreeName("KeyManagerPanel.SSHCopyIdTree")
|
||||||
hostTreeDialog.isVisible = true
|
hostTreeDialog.isVisible = true
|
||||||
val hosts = hostTreeDialog.hosts
|
val hosts = hostTreeDialog.hosts
|
||||||
if (hosts.isEmpty()) {
|
if (hosts.isEmpty()) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SSHCopyIdDialog(
|
|||||||
}
|
}
|
||||||
private val terminalPanel by lazy {
|
private val terminalPanel by lazy {
|
||||||
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
|
||||||
|
.apply { enableFloatingToolbar = false }
|
||||||
}
|
}
|
||||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
@@ -152,7 +153,7 @@ class SSHCopyIdDialog(
|
|||||||
val baos = ByteArrayOutputStream()
|
val baos = ByteArrayOutputStream()
|
||||||
channel.out = baos
|
channel.out = baos
|
||||||
if (channel.open().verify(timeout).await(timeout)) {
|
if (channel.open().verify(timeout).await(timeout)) {
|
||||||
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout);
|
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout)
|
||||||
}
|
}
|
||||||
if (channel.exitStatus != 0) {
|
if (channel.exitStatus != 0) {
|
||||||
throw IllegalStateException("Server response: ${channel.exitStatus}")
|
throw IllegalStateException("Server response: ${channel.exitStatus}")
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
private var visualWindows = emptyArray<VisualWindow>()
|
private var visualWindows = emptyArray<VisualWindow>()
|
||||||
|
|
||||||
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
|
||||||
|
var enableFloatingToolbar = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||||
|
} else {
|
||||||
|
layeredPane.remove(floatingToolbar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,7 +134,9 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
|||||||
|
|
||||||
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
|
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
|
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
|
||||||
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
if (enableFloatingToolbar) {
|
||||||
|
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
|
||||||
|
}
|
||||||
add(layeredPane, BorderLayout.CENTER)
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
add(scrollBar, BorderLayout.EAST)
|
add(scrollBar, BorderLayout.EAST)
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ class FileSystemTabbed(
|
|||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener {
|
addBtn.addActionListener {
|
||||||
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
val dialog = NewHostTreeDialog(SwingUtilities.getWindowAncestor(this))
|
||||||
dialog.location = Point(
|
dialog.location = Point(
|
||||||
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
max(0, addBtn.locationOnScreen.x - dialog.width / 2 + addBtn.width / 2),
|
||||||
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
addBtn.locationOnScreen.y + max(tabHeight, addBtn.height)
|
||||||
)
|
)
|
||||||
|
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||||
|
dialog.setTreeName("FileSystemTabbed.Tree")
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
for (host in dialog.hosts) {
|
for (host in dialog.hosts) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.termora.transport
|
package app.termora.transport
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon
|
||||||
@@ -291,9 +293,11 @@ class SftpFileSystemPanel(
|
|||||||
val builder = FormBuilder.create().layout(layout).debug(false)
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
builder.add(FlatOptionPaneInformationIcon()).xy(2, 2)
|
builder.add(FlatOptionPaneInformationIcon()).xy(2, 2)
|
||||||
builder.add(errorInfo).xyw(1, 4, 3, "fill, center")
|
builder.add(errorInfo).xyw(1, 4, 3, "fill, center")
|
||||||
builder.add(JXHyperlink(object : AbstractAction(I18n.getString("termora.transport.sftp.select-host")) {
|
builder.add(JXHyperlink(object : AnAction(I18n.getString("termora.transport.sftp.select-host")) {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val dialog = HostTreeDialog(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
|
val dialog = NewHostTreeDialog(evt.window)
|
||||||
|
dialog.setFilter { it.host.protocol == Protocol.SSH }
|
||||||
|
dialog.setTreeName("SftpFileSystemPanel.SelectHostTree")
|
||||||
dialog.allowMulti = false
|
dialog.allowMulti = false
|
||||||
dialog.setLocationRelativeTo(this@SelectHostPanel)
|
dialog.setLocationRelativeTo(this@SelectHostPanel)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ termora.welcome.my-hosts=My hosts
|
|||||||
termora.welcome.contextmenu.connect=Connect
|
termora.welcome.contextmenu.connect=Connect
|
||||||
termora.welcome.contextmenu.connect-with=Connect with
|
termora.welcome.contextmenu.connect-with=Connect with
|
||||||
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
termora.welcome.contextmenu.open-in-new-window=${termora.tabbed.contextmenu.open-in-new-window}
|
||||||
|
termora.welcome.contextmenu.refresh=${termora.transport.table.contextmenu.refresh}
|
||||||
termora.welcome.contextmenu.copy=${termora.copy}
|
termora.welcome.contextmenu.copy=${termora.copy}
|
||||||
termora.welcome.contextmenu.remove=${termora.remove}
|
termora.welcome.contextmenu.remove=${termora.remove}
|
||||||
termora.welcome.contextmenu.rename=Rename
|
termora.welcome.contextmenu.rename=Rename
|
||||||
@@ -350,7 +351,7 @@ termora.visual-window.system-information.filesystem=Filesystem
|
|||||||
termora.visual-window.system-information.used-total=Used / Total
|
termora.visual-window.system-information.used-total=Used / Total
|
||||||
|
|
||||||
|
|
||||||
termora.visual-window.nvidia-smi=NVIDIA System Management Interface
|
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||||
|
|
||||||
|
|
||||||
termora.floating-toolbar.not-supported=This action is not supported
|
termora.floating-toolbar.not-supported=This action is not supported
|
||||||
|
|||||||
Reference in New Issue
Block a user