feat: refactoring HostTree & support sorting (#285)

This commit is contained in:
hstyi
2025-02-21 16:24:45 +08:00
committed by GitHub
parent 219e5420f5
commit 7df317a1b9
22 changed files with 1102 additions and 1119 deletions

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

View File

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

View File

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

View File

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

View File

@@ -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}
&nbsp;&nbsp;
<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}
&nbsp;&nbsp;
<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
}
}
}

View File

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

View File

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

View 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()
}
}

View 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}&nbsp;&nbsp;&nbsp;&nbsp;${fontTag.apply("${host.username}@${host.host}")}</html>"
} else if (host.protocol == Protocol.Serial) {
text =
"<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;${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)
}
}
}

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

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

View File

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

View File

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

View File

@@ -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}&nbsp;&nbsp;&nbsp;&nbsp;<font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>" return "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;<font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"

View File

@@ -17,6 +17,6 @@ object DataProviders {
object Welcome { object Welcome {
val HostTree = DataKey(app.termora.HostTree::class) val HostTree = DataKey(app.termora.NewHostTree::class)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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