mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
feat: support snippet (#321)
This commit is contained in:
@@ -5,6 +5,7 @@ import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.sync.SyncType
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
@@ -12,7 +13,6 @@ import jetbrains.exodus.env.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -26,6 +26,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
companion object {
|
||||
private const val KEYMAP_STORE = "Keymap"
|
||||
private const val HOST_STORE = "Host"
|
||||
private const val SNIPPET_STORE = "Snippet"
|
||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||
private const val MACRO_STORE = "Macro"
|
||||
private const val KEY_PAIR_STORE = "KeyPair"
|
||||
@@ -105,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllHost() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
it.deleteCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllKeyPair() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
@@ -152,15 +142,32 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
var text = ohMyJson.encodeToString(snippet)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
delete(it, HOST_STORE, id)
|
||||
put(it, SNIPPET_STORE, snippet.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Host: $id")
|
||||
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getSnippets(): Collection<Snippet> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||
@@ -621,6 +628,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var rangeHosts by BooleanPropertyDelegate(true)
|
||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||
var rangeMacros by BooleanPropertyDelegate(true)
|
||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||
|
||||
@@ -41,7 +41,7 @@ class FilterableHostTreeModel(
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.host.protocol != Protocol.Folder) {
|
||||
if (c.data.protocol != Protocol.Folder) {
|
||||
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
||||
companion object {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
}
|
||||
|
||||
var host: Host
|
||||
get() = data
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
override val isFolder: Boolean
|
||||
get() = data.protocol == Protocol.Folder
|
||||
|
||||
override val id: String
|
||||
get() = data.id
|
||||
|
||||
/**
|
||||
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
|
||||
*/
|
||||
var host: Host
|
||||
override var data: Host
|
||||
get() {
|
||||
val cacheHost = hostManager.getHost((userObject as Host).id)
|
||||
val myHost = userObject as Host
|
||||
@@ -22,22 +34,23 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
|
||||
override val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.data.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())
|
||||
}
|
||||
override fun getAllChildren(): List<HostTreeNode> {
|
||||
return super.getAllChildren().filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||
return when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
fun childrenNode(): List<HostTreeNode> {
|
||||
@@ -57,7 +70,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
|
||||
for (child in oldNode.childrenNode()) {
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.data.protocol)) continue
|
||||
val newChildNode = child.clone() as HostTreeNode
|
||||
deepClone(newChildNode, child, scopes)
|
||||
newNode.add(newChildNode)
|
||||
@@ -65,7 +78,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
|
||||
override fun clone(): Any {
|
||||
val newNode = HostTreeNode(host)
|
||||
val newNode = HostTreeNode(data)
|
||||
newNode.children = null
|
||||
newNode.parent = null
|
||||
return newNode
|
||||
@@ -74,7 +87,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
override fun isNodeChild(aNode: TreeNode?): Boolean {
|
||||
if (aNode is HostTreeNode) {
|
||||
for (node in childrenNode()) {
|
||||
if (node.host == aNode.host) {
|
||||
if (node.data == aNode.data) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -88,10 +101,10 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
other as HostTreeNode
|
||||
|
||||
return host == other.host
|
||||
return data == other.data
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return host.hashCode()
|
||||
return data.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ object Icons {
|
||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||
val anyType by lazy { DynamicIcon("icons/anyType.svg", "icons/anyType_dark.svg") }
|
||||
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
||||
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
|
||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||
val applyNotConflictsLeft by lazy {
|
||||
|
||||
@@ -5,8 +5,6 @@ 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 kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.csv.CSVFormat
|
||||
@@ -19,43 +17,31 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.ini4j.Ini
|
||||
import org.ini4j.Reg
|
||||
import org.jdesktop.swingx.JXTree
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
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.*
|
||||
import java.io.*
|
||||
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.filechooser.FileNameExtensionFilter
|
||||
import javax.swing.tree.TreePath
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
import kotlin.math.min
|
||||
|
||||
class NewHostTree : JXTree() {
|
||||
class NewHostTree : SimpleTree() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
|
||||
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -64,7 +50,7 @@ class NewHostTree : JXTree() {
|
||||
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||
|
||||
private val model = NewHostTreeModel()
|
||||
override val model = NewHostTreeModel()
|
||||
|
||||
/**
|
||||
* 是否允许显示右键菜单
|
||||
@@ -89,7 +75,6 @@ class NewHostTree : JXTree() {
|
||||
isRootVisible = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
editor.preferredSize = Dimension(220, 0)
|
||||
|
||||
// renderer
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
@@ -135,74 +120,16 @@ class NewHostTree : JXTree() {
|
||||
|
||||
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
|
||||
}
|
||||
icon = node.getIcon(sel, expanded, hasFocus)
|
||||
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() {
|
||||
// 右键选中
|
||||
// double click
|
||||
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
|
||||
@@ -216,7 +143,7 @@ class NewHostTree : JXTree() {
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
||||
val nodes = getSelectionHostTreeNodes(false)
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
|
||||
val path = TreePath(model.getPathToRoot(nodes.first()))
|
||||
if (isExpanded(path)) {
|
||||
@@ -225,7 +152,7 @@ class NewHostTree : JXTree() {
|
||||
expandPath(path)
|
||||
}
|
||||
} else {
|
||||
for (node in getSelectionHostTreeNodes(true)) {
|
||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e))
|
||||
}
|
||||
}
|
||||
@@ -233,145 +160,16 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
})
|
||||
|
||||
// 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, updateDate = System.currentTimeMillis())
|
||||
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, updateDate = System.currentTimeMillis())
|
||||
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) {
|
||||
override fun showContextmenu(evt: MouseEvent) {
|
||||
if (!contextmenu) return
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is HostTreeNode) return
|
||||
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
val fullNodes = getSelectionSimpleTreeNodes(true)
|
||||
val lastNodeParent = lastNode.parent ?: model.root
|
||||
val lastHost = lastNode.host
|
||||
|
||||
@@ -443,7 +241,6 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
remove.addActionListener(object : ActionListener {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val nodes = getSelectionHostTreeNodes()
|
||||
if (nodes.isEmpty()) return
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(tree),
|
||||
@@ -470,7 +267,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
})
|
||||
copy.addActionListener {
|
||||
for (c in getSelectionHostTreeNodes()) {
|
||||
for (c in nodes) {
|
||||
val p = c.parent ?: continue
|
||||
val newNode = copyNode(c, p.host.id)
|
||||
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
|
||||
@@ -479,12 +276,12 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||
expandAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true)) {
|
||||
for (node in fullNodes) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
colspanAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true).reversed()) {
|
||||
for (node in fullNodes.reversed()) {
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
@@ -512,29 +309,10 @@ class NewHostTree : JXTree() {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh.addActionListener { refreshNode(lastNode) }
|
||||
|
||||
newMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root }
|
||||
remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
|
||||
copy.isEnabled = remove.isEnabled
|
||||
rename.isEnabled = remove.isEnabled
|
||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||
@@ -542,28 +320,27 @@ class NewHostTree : JXTree() {
|
||||
importMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
|
||||
// 如果选中了 SSH 服务器,那么才启用
|
||||
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH }
|
||||
openWithSFTP.isEnabled = fullNodes.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)
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||
val lastNode = node as? HostTreeNode ?: return
|
||||
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
|
||||
model.nodeStructureChanged(lastNode)
|
||||
hostManager.addHost(lastNode.host)
|
||||
}
|
||||
|
||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
val nNode = node as? HostTreeNode ?: return
|
||||
val nParent = parent as? HostTreeNode ?: return
|
||||
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(nNode.host)
|
||||
}
|
||||
|
||||
|
||||
private fun copyNode(
|
||||
node: HostTreeNode,
|
||||
parentId: String,
|
||||
@@ -600,30 +377,14 @@ class NewHostTree : JXTree() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
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
|
||||
override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
|
||||
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
|
||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||
assertEventDispatchThread()
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
if (nodes.isEmpty()) return
|
||||
val source = if (openInNewWindow)
|
||||
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||
@@ -632,7 +393,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
private fun openWithSFTP(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(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
|
||||
@@ -643,7 +404,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
private fun openWithSFTPCommand(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(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))
|
||||
@@ -1106,28 +867,5 @@ class NewHostTree : JXTree() {
|
||||
electerm,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class NewHostTreeDialog(
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
hosts = tree.getSelectionHostTreeNodes(true)
|
||||
hosts = tree.getSelectionSimpleTreeNodes(true)
|
||||
.filter { filter.apply(it) }
|
||||
.map { it.host }
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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(
|
||||
class NewHostTreeModel : SimpleTreeModel<Host>(
|
||||
HostTreeNode(
|
||||
Host(
|
||||
id = "0",
|
||||
|
||||
@@ -15,6 +15,8 @@ import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.native.FileChooser
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.snippet.SnippetManager
|
||||
import app.termora.sync.SyncConfig
|
||||
import app.termora.sync.SyncRange
|
||||
import app.termora.sync.SyncType
|
||||
@@ -67,6 +69,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||
private val database get() = Database.getDatabase()
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val macroManager get() = MacroManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
@@ -561,6 +564,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val sync get() = database.sync
|
||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||
val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title"))
|
||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
||||
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||
@@ -665,6 +669,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
keysCheckBox.addActionListener { refreshButtons() }
|
||||
hostsCheckBox.addActionListener { refreshButtons() }
|
||||
snippetsCheckBox.addActionListener { refreshButtons() }
|
||||
keywordHighlightsCheckBox.addActionListener { refreshButtons() }
|
||||
|
||||
}
|
||||
@@ -672,6 +677,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun refreshButtons() {
|
||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||
sync.rangeHosts = hostsCheckBox.isSelected
|
||||
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||
|
||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
@@ -848,6 +854,17 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.Snippets)) {
|
||||
val snippets = json["snippets"]
|
||||
if (snippets is JsonArray) {
|
||||
ohMyJson.runCatching { decodeFromJsonElement<List<Snippet>>(snippets.jsonArray) }.onSuccess {
|
||||
for (snippet in it) {
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keyPairs = json["keyPairs"]
|
||||
if (keyPairs is JsonArray) {
|
||||
@@ -909,6 +926,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.Snippets)) {
|
||||
put("snippets", ohMyJson.encodeToJsonElement(snippetManager.snippets()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||
}
|
||||
@@ -978,6 +998,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (keymapCheckBox.isSelected) {
|
||||
range.add(SyncRange.Keymap)
|
||||
}
|
||||
if (snippetsCheckBox.isSelected) {
|
||||
range.add(SyncRange.Snippets)
|
||||
}
|
||||
return SyncConfig(
|
||||
type = typeComboBox.selectedItem as SyncType,
|
||||
token = String(tokenTextField.password),
|
||||
@@ -1054,6 +1077,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
keymapCheckBox.isEnabled = false
|
||||
keywordHighlightsCheckBox.isEnabled = false
|
||||
hostsCheckBox.isEnabled = false
|
||||
snippetsCheckBox.isEnabled = false
|
||||
domainTextField.isEnabled = false
|
||||
|
||||
if (push) {
|
||||
@@ -1083,6 +1107,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
uploadConfigButton.isEnabled = true
|
||||
keysCheckBox.isEnabled = true
|
||||
hostsCheckBox.isEnabled = true
|
||||
snippetsCheckBox.isEnabled = true
|
||||
typeComboBox.isEnabled = true
|
||||
macrosCheckBox.isEnabled = true
|
||||
keymapCheckBox.isEnabled = true
|
||||
@@ -1144,12 +1169,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
typeComboBox.addItem(SyncType.WebDAV)
|
||||
|
||||
hostsCheckBox.isFocusable = false
|
||||
snippetsCheckBox.isFocusable = false
|
||||
keysCheckBox.isFocusable = false
|
||||
keywordHighlightsCheckBox.isFocusable = false
|
||||
macrosCheckBox.isFocusable = false
|
||||
keymapCheckBox.isFocusable = false
|
||||
|
||||
hostsCheckBox.isSelected = sync.rangeHosts
|
||||
snippetsCheckBox.isSelected = sync.rangeSnippets
|
||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||
macrosCheckBox.isSelected = sync.rangeMacros
|
||||
@@ -1236,7 +1263,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, left:pref, $formMargin, left:pref",
|
||||
"pref, $formMargin, pref"
|
||||
"pref, 2dlu, pref"
|
||||
)
|
||||
)
|
||||
.add(hostsCheckBox).xy(1, 1)
|
||||
@@ -1244,6 +1271,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(keywordHighlightsCheckBox).xy(5, 1)
|
||||
.add(macrosCheckBox).xy(1, 3)
|
||||
.add(keymapCheckBox).xy(3, 3)
|
||||
.add(snippetsCheckBox).xy(5, 3)
|
||||
.build()
|
||||
|
||||
var rows = 1
|
||||
@@ -1612,13 +1640,15 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
val hosts = hostManager.hosts()
|
||||
val keyPairs = keyManager.getOhKeyPairs()
|
||||
val snippets = snippetManager.snippets()
|
||||
|
||||
// 获取到安全的属性,如果设置密码那表示之前并未加密
|
||||
// 这里取出来之后重新存储加密
|
||||
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
|
||||
|
||||
val key = doorman.work(passwordTextField.password)
|
||||
|
||||
hosts.forEach { hostManager.addHost(it) }
|
||||
snippets.forEach { snippetManager.addSnippet(it) }
|
||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||
for (e in properties) {
|
||||
for ((k, v) in e.second) {
|
||||
|
||||
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
@@ -0,0 +1,343 @@
|
||||
package app.termora
|
||||
|
||||
import org.jdesktop.swingx.JXTree
|
||||
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.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.tree.TreePath
|
||||
import kotlin.math.min
|
||||
|
||||
open class SimpleTree : JXTree() {
|
||||
|
||||
protected open val model get() = super.getModel() as SimpleTreeModel<*>
|
||||
private val editor = OutlineTextField(64)
|
||||
protected val tree get() = this
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
|
||||
// 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 SimpleTreeNode<*>
|
||||
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||
icon = node.getIcon(sel, expanded, hasFocus)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent || !tree.isCellEditable(e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return super.isCellEditable(e).apply {
|
||||
if (this) {
|
||||
editor.preferredSize = Dimension(min(220, width - 64), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCellEditorValue(): Any? {
|
||||
return getLastSelectedPathNode()?.data
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
showContextmenu(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val node = getLastSelectedPathNode() ?: return
|
||||
if (editor.text.isBlank() || editor.text == node.toString()) {
|
||||
return
|
||||
}
|
||||
onRenamed(node, editor.text)
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable? {
|
||||
val nodes = getSelectionSimpleTreeNodes().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 MoveNodeTransferable(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 path = dropLocation.path ?: return false
|
||||
val node = path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
if (!support.isDataFlavorSupported(MoveNodeTransferable.dataFlavor)) return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
if (nodes.isEmpty()) return false
|
||||
if (!node.isFolder) return false
|
||||
|
||||
for (e in nodes) {
|
||||
// 禁止拖拽到自己的子下面
|
||||
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件夹只能拖拽到文件夹的下面
|
||||
if (e.isFolder) {
|
||||
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? SimpleTreeNode<*> ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
|
||||
// 展开的 node
|
||||
val expanded = mutableSetOf(node.id)
|
||||
for (e in nodes) {
|
||||
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||
.map { it }.forEach { expanded.add(it.id) }
|
||||
}
|
||||
|
||||
// 转移
|
||||
for (e in nodes) {
|
||||
model.removeNodeFromParent(e)
|
||||
rebase(e, node)
|
||||
|
||||
if (dropLocation.childIndex == -1) {
|
||||
if (e.isFolder) {
|
||||
model.insertNodeInto(e, node, node.folderCount)
|
||||
} else {
|
||||
model.insertNodeInto(e, node, node.childCount)
|
||||
}
|
||||
} else {
|
||||
if (e.isFolder) {
|
||||
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.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun newFolder(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.folderCount)
|
||||
}
|
||||
|
||||
protected open fun newFile(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.childCount)
|
||||
}
|
||||
|
||||
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
model.insertNodeInto(newNode, lastNode, index)
|
||||
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||
startEditingAtPath(selectionPath)
|
||||
return true
|
||||
}
|
||||
|
||||
open fun getLastSelectedPathNode(): SimpleTreeNode<*>? {
|
||||
return lastSelectedPathComponent as? SimpleTreeNode<*>
|
||||
}
|
||||
|
||||
|
||||
protected open fun showContextmenu(evt: MouseEvent) {
|
||||
|
||||
}
|
||||
|
||||
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||
|
||||
protected open fun refreshNode(node: SimpleTreeNode<*>) {
|
||||
val state = TreeUtils.saveExpansionState(tree)
|
||||
val rows = selectionRows
|
||||
|
||||
model.reload(node)
|
||||
|
||||
TreeUtils.loadExpansionState(tree, state)
|
||||
|
||||
super.setSelectionRows(rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
open fun getSelectionSimpleTreeNodes(include: Boolean = false): List<SimpleTreeNode<*>> {
|
||||
val paths = selectionPaths ?: return emptyList()
|
||||
if (paths.isEmpty()) return emptyList()
|
||||
val nodes = mutableListOf<SimpleTreeNode<*>>()
|
||||
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||
.filterIsInstance<SimpleTreeNode<*>>().toMutableList()
|
||||
|
||||
if (include) {
|
||||
while (parents.isNotEmpty()) {
|
||||
val node = parents.removeFirst()
|
||||
nodes.add(node)
|
||||
parents.addAll(node.children().toList().filterIsInstance<SimpleTreeNode<*>>())
|
||||
}
|
||||
}
|
||||
|
||||
return if (include) nodes else parents
|
||||
}
|
||||
|
||||
protected open fun isCellEditable(e: EventObject?): Boolean {
|
||||
return getLastSelectedPathNode() != model.root
|
||||
}
|
||||
|
||||
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
|
||||
}
|
||||
|
||||
private class MoveNodeTransferable(val nodes: List<SimpleTreeNode<*>>) : Transferable {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveNodeTransferable::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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
|
||||
abstract class SimpleTreeModel<T>(root: SimpleTreeNode<T>) : DefaultTreeModel(root) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getRoot(): SimpleTreeNode<T> {
|
||||
return super.getRoot() as SimpleTreeNode<T>
|
||||
}
|
||||
|
||||
}
|
||||
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
|
||||
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open var data: T
|
||||
get() = userObject as T
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getParent(): SimpleTreeNode<T>? {
|
||||
return super.getParent() as SimpleTreeNode<T>?
|
||||
}
|
||||
|
||||
open val folderCount: Int get() = 0
|
||||
|
||||
open fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon? {
|
||||
return null
|
||||
}
|
||||
|
||||
open val isFolder get() = false
|
||||
|
||||
abstract val id: String
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun getAllChildren(): List<SimpleTreeNode<T>> {
|
||||
val children = mutableListOf<SimpleTreeNode<T>>()
|
||||
for (child in children()) {
|
||||
val c = child as? SimpleTreeNode<T> ?: continue
|
||||
children.add(c)
|
||||
children.addAll(c.getAllChildren())
|
||||
}
|
||||
return children
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface TerminalTab : Disposable {
|
||||
interface TerminalTab : Disposable, DataProvider {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.WindowDecorations
|
||||
@@ -42,6 +43,7 @@ class TermoraToolBar(
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.JTree
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
object TreeUtils {
|
||||
/**
|
||||
@@ -31,16 +31,6 @@ object TreeUtils {
|
||||
return nodes
|
||||
}
|
||||
|
||||
fun parents(node: TreeNode): List<Any> {
|
||||
val parents = mutableListOf<Any>()
|
||||
var p = node.parent
|
||||
while (p != null) {
|
||||
parents.add(p)
|
||||
p = p.parent
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
fun saveExpansionState(tree: JTree): String {
|
||||
val rows = mutableListOf<Int>()
|
||||
for (i in 0 until tree.rowCount) {
|
||||
@@ -63,15 +53,15 @@ object TreeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun expandAll(tree: JTree) {
|
||||
var j = tree.rowCount
|
||||
var i = 0
|
||||
while (i < j) {
|
||||
tree.expandRow(i)
|
||||
i += 1
|
||||
j = tree.rowCount
|
||||
fun saveSelectionRows(tree: JTree): String {
|
||||
return tree.selectionRows?.joinToString(",") ?: StringUtils.EMPTY
|
||||
}
|
||||
|
||||
fun loadSelectionRows(tree: JTree, state: String) {
|
||||
if (state.isBlank()) return
|
||||
for (row in state.split(",").mapNotNull { it.toIntOrNull() }) {
|
||||
tree.addSelectionRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.highlight.KeywordHighlightAction
|
||||
import app.termora.keymgr.KeyManagerAction
|
||||
import app.termora.macro.MacroAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import app.termora.tlog.TerminalLoggerAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import javax.swing.Action
|
||||
@@ -34,6 +35,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, SFTPAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ interface DataProvider {
|
||||
/**
|
||||
* 数据提供
|
||||
*/
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T?
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T? = null
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
|
||||
object DataProviders {
|
||||
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
||||
val PtyConnector get() = DataKey.PtyConnector
|
||||
|
||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.jdesktop.swingx.action.AbstractActionExt
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.Action
|
||||
import javax.swing.Icon
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
|
||||
private val isState: Boolean
|
||||
@@ -26,7 +27,7 @@ open class ActionFindEverywhereResult(private val action: Action) : FindEverywhe
|
||||
if (isState) {
|
||||
action.putValue(Action.SELECTED_KEY, !isSelected)
|
||||
}
|
||||
action.actionPerformed(e)
|
||||
SwingUtilities.invokeLater { action.actionPerformed(e) }
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenLocalTerminalAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import javax.swing.Icon
|
||||
@@ -21,6 +22,11 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// Snippet
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// SFTP
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
|
||||
24
src/main/kotlin/app/termora/snippet/Snippet.kt
Normal file
24
src/main/kotlin/app/termora/snippet/Snippet.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.toSimpleString
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
enum class SnippetType {
|
||||
Folder,
|
||||
Snippet,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Snippet(
|
||||
val id: String = UUID.randomUUID().toSimpleString(),
|
||||
val name: String,
|
||||
val snippet: String = StringUtils.EMPTY,
|
||||
val parentId: String = StringUtils.EMPTY,
|
||||
val type: SnippetType = SnippetType.Snippet,
|
||||
val deleted: Boolean = false,
|
||||
val sort: Long = System.currentTimeMillis(),
|
||||
val createDate: Long = System.currentTimeMillis(),
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
)
|
||||
47
src/main/kotlin/app/termora/snippet/SnippetAction.kt
Normal file
47
src/main/kotlin/app/termora/snippet/SnippetAction.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.Terminal
|
||||
|
||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||
companion object {
|
||||
fun getInstance(): SnippetAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(SnippetAction::class) { SnippetAction() }
|
||||
}
|
||||
|
||||
const val SNIPPET = "SnippetAction"
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
SnippetDialog(evt.window).isVisible = true
|
||||
}
|
||||
|
||||
|
||||
fun runSnippet(snippet: Snippet, terminal: Terminal) {
|
||||
if (snippet.type != SnippetType.Snippet) return
|
||||
val terminalModel = terminal.getTerminalModel()
|
||||
val map = mapOf(
|
||||
"\\r" to ControlCharacters.CR,
|
||||
"\\n" to ControlCharacters.LF,
|
||||
"\\t" to ControlCharacters.TAB,
|
||||
"\\a" to ControlCharacters.BEL,
|
||||
"\\e" to ControlCharacters.ESC,
|
||||
"\\b" to ControlCharacters.BS,
|
||||
)
|
||||
|
||||
if (terminalModel.hasData(DataKey.PtyConnector)) {
|
||||
var text = snippet.snippet
|
||||
for (e in map.entries) {
|
||||
text = text.replace(e.key, e.value.toString())
|
||||
}
|
||||
val ptyConnector = terminalModel.getData(DataKey.PtyConnector)
|
||||
ptyConnector.write(text.toByteArray(ptyConnector.getCharset()))
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/main/kotlin/app/termora/snippet/SnippetBannerPanel.kt
Normal file
50
src/main/kotlin/app/termora/snippet/SnippetBannerPanel.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import java.awt.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SnippetBannerPanel(fontSize: Int = 12) : JComponent() {
|
||||
private val banner = """
|
||||
_____ _ __
|
||||
/ ___/____ (_)___ ____ ___ / /_
|
||||
\__ \/ __ \/ / __ \/ __ \/ _ \/ __/
|
||||
___/ / / / / / /_/ / /_/ / __/ /_
|
||||
/____/_/ /_/_/ .___/ .___/\___/\__/
|
||||
/_/ /_/
|
||||
""".trimIndent().lines()
|
||||
|
||||
init {
|
||||
font = Font("JetBrains Mono", Font.PLAIN, fontSize)
|
||||
preferredSize = Dimension(width, getFontMetrics(font).height * banner.size)
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (g is Graphics2D) {
|
||||
g.setRenderingHints(
|
||||
RenderingHints(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
g.font = font
|
||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||
|
||||
val height = g.fontMetrics.height
|
||||
val descent = g.fontMetrics.descent
|
||||
val offset = width / 2 - g.fontMetrics.stringWidth(banner.maxBy { it.length }) / 2
|
||||
|
||||
for (i in banner.indices) {
|
||||
var x = offset
|
||||
val y = height * (i + 1) - descent
|
||||
val chars = banner[i].toCharArray()
|
||||
for (j in chars.indices) {
|
||||
g.drawChars(chars, j, 1, x, y)
|
||||
x += g.fontMetrics.charWidth(chars[j])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
59
src/main/kotlin/app/termora/snippet/SnippetDialog.kt
Normal file
59
src/main/kotlin/app/termora/snippet/SnippetDialog.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
import kotlin.math.max
|
||||
|
||||
class SnippetDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
init()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val w = properties.getString("SnippetDialog.width", "0").toIntOrNull() ?: 0
|
||||
val h = properties.getString("SnippetDialog.height", "0").toIntOrNull() ?: 0
|
||||
val x = properties.getString("SnippetDialog.x", "-1").toIntOrNull() ?: -1
|
||||
val y = properties.getString("SnippetDialog.y", "-1").toIntOrNull() ?: -1
|
||||
|
||||
size = if (w > 0 && h > 0) {
|
||||
Dimension(w, h)
|
||||
} else {
|
||||
Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
}
|
||||
isModal = true
|
||||
isResizable = true
|
||||
title = I18n.getString("termora.snippet.title")
|
||||
|
||||
if (x != -1 && y != -1) {
|
||||
setLocation(max(x, 0), max(y, 0))
|
||||
} else {
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetDialog.width", width.toString())
|
||||
properties.putString("SnippetDialog.height", height.toString())
|
||||
properties.putString("SnippetDialog.x", x.toString())
|
||||
properties.putString("SnippetDialog.y", y.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return SnippetPanel().apply { Disposer.register(disposable, this) }
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
44
src/main/kotlin/app/termora/snippet/SnippetManager.kt
Normal file
44
src/main/kotlin/app/termora/snippet/SnippetManager.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.assertEventDispatchThread
|
||||
|
||||
|
||||
class SnippetManager private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): SnippetManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(SnippetManager::class) { SnippetManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val database get() = Database.getDatabase()
|
||||
private var snippets = mutableMapOf<String, Snippet>()
|
||||
|
||||
/**
|
||||
* 修改缓存并存入数据库
|
||||
*/
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
assertEventDispatchThread()
|
||||
database.addSnippet(snippet)
|
||||
if (snippet.deleted) {
|
||||
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id }
|
||||
} else {
|
||||
snippets[snippet.id] = snippet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
fun snippets(): List<Snippet> {
|
||||
if (snippets.isEmpty()) {
|
||||
database.getSnippets().filter { !it.deleted }
|
||||
.forEach { snippets[it.id] = it }
|
||||
}
|
||||
return snippets.values.filter { !it.deleted }
|
||||
.sortedWith(compareBy<Snippet> { if (it.type == SnippetType.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
225
src/main/kotlin/app/termora/snippet/SnippetPanel.kt
Normal file
225
src/main/kotlin/app/termora/snippet/SnippetPanel.kt
Normal file
@@ -0,0 +1,225 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.components.FlatTextArea
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.undo.UndoManager
|
||||
|
||||
|
||||
class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SnippetPanel::class.java)
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
}
|
||||
|
||||
private val leftPanel = JPanel(BorderLayout())
|
||||
private val cardLayout = CardLayout()
|
||||
private val rightPanel = JPanel(cardLayout)
|
||||
private val snippetTree = SnippetTree()
|
||||
private val editor = SnippetEditor()
|
||||
private val lastNode get() = snippetTree.getLastSelectedPathNode()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val splitPane = JSplitPane()
|
||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
)
|
||||
leftPanel.preferredSize = Dimension(
|
||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||
-1
|
||||
)
|
||||
|
||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(6, 6, 6, 6)
|
||||
)
|
||||
|
||||
val bannerPanel = JPanel(BorderLayout())
|
||||
bannerPanel.add(SnippetBannerPanel(), BorderLayout.CENTER)
|
||||
bannerPanel.border = BorderFactory.createEmptyBorder(32, 0, 0, 0)
|
||||
rightPanel.add(bannerPanel, "Banner")
|
||||
rightPanel.add(editor, "Editor")
|
||||
|
||||
splitPane.leftComponent = leftPanel
|
||||
splitPane.rightComponent = rightPanel
|
||||
add(splitPane, BorderLayout.CENTER)
|
||||
|
||||
cardLayout.show(rightPanel, "Banner")
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
snippetTree.addTreeSelectionListener {
|
||||
val lastNode = this.lastNode
|
||||
if (lastNode == null || lastNode.isFolder) {
|
||||
cardLayout.show(rightPanel, "Banner")
|
||||
} else {
|
||||
cardLayout.show(rightPanel, "Editor")
|
||||
editor.textArea.text = lastNode.data.snippet
|
||||
editor.resetUndo()
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
if (snippetTree.selectionRows?.isEmpty() == true) {
|
||||
snippetTree.addSelectionRow(0)
|
||||
}
|
||||
snippetTree.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
val expansionState = properties.getString("SnippetPanel.LeftTreePanel.expansionState", StringUtils.EMPTY)
|
||||
if (expansionState.isNotBlank()) {
|
||||
TreeUtils.loadExpansionState(snippetTree, expansionState)
|
||||
}
|
||||
|
||||
val selectionRows = properties.getString("SnippetPanel.LeftTreePanel.selectionRows", StringUtils.EMPTY)
|
||||
if (selectionRows.isNotBlank()) {
|
||||
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetPanel.LeftPanel.width", leftPanel.width.toString())
|
||||
properties.putString("SnippetPanel.LeftPanel.height", leftPanel.height.toString())
|
||||
properties.putString("SnippetPanel.LeftTreePanel.expansionState", TreeUtils.saveExpansionState(snippetTree))
|
||||
properties.putString("SnippetPanel.LeftTreePanel.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
|
||||
}
|
||||
|
||||
|
||||
private inner class SnippetEditor : JPanel(BorderLayout()) {
|
||||
val textArea = FlatTextArea()
|
||||
private var undoManager = UndoManager()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(JScrollPane(textArea).apply { border = BorderFactory.createEmptyBorder() }, BorderLayout.CENTER)
|
||||
panel.border = FlatRoundBorder()
|
||||
add(panel, BorderLayout.CENTER)
|
||||
add(createTip(), BorderLayout.SOUTH)
|
||||
|
||||
textArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
textArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
textArea.document.addUndoableEditListener(undoManager)
|
||||
|
||||
textArea.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if ((e.keyCode == KeyEvent.VK_Z || e.keyCode == KeyEvent.VK_Y) && (if (SystemInfo.isMacOS) e.isMetaDown else e.isControlDown)) {
|
||||
try {
|
||||
if (e.keyCode == KeyEvent.VK_Z) {
|
||||
if (undoManager.canUndo()) {
|
||||
undoManager.undo()
|
||||
}
|
||||
} else {
|
||||
if (undoManager.canRedo()) {
|
||||
undoManager.redo()
|
||||
}
|
||||
}
|
||||
} catch (cue: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(cue.message, cue.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
val lastNode = lastNode ?: return
|
||||
lastNode.data = lastNode.data.copy(snippet = textArea.text, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(lastNode.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun resetUndo() {
|
||||
textArea.document.removeUndoableEditListener(undoManager)
|
||||
undoManager = UndoManager()
|
||||
textArea.document.addUndoableEditListener(undoManager)
|
||||
}
|
||||
|
||||
private fun createTip(): JPanel {
|
||||
val formMargin = "10dlu"
|
||||
val panel = FormBuilder.create().debug(false)
|
||||
.border(
|
||||
BorderFactory.createCompoundBorder(
|
||||
FlatRoundBorder(),
|
||||
BorderFactory.createEmptyBorder(2, 4, 4, 4)
|
||||
)
|
||||
)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, left:pref, $formMargin, left:pref, left:pref, $formMargin, left:pref, left:pref",
|
||||
"pref, $formMargin, pref"
|
||||
)
|
||||
)
|
||||
.add(createTipLabel("\\r - ")).xy(1, 1)
|
||||
.add(createTipLabel("CR")).xy(2, 1)
|
||||
.add(createTipLabel("\\n - ")).xy(4, 1)
|
||||
.add(createTipLabel("LF")).xy(5, 1)
|
||||
.add(createTipLabel("\\t - ")).xy(7, 1)
|
||||
.add(createTipLabel("Tab")).xy(8, 1)
|
||||
|
||||
.add(createTipLabel("\\a - ")).xy(1, 2)
|
||||
.add(createTipLabel("Bell")).xy(2, 2)
|
||||
.add(createTipLabel("\\e - ")).xy(4, 2)
|
||||
.add(createTipLabel("Escape")).xy(5, 2)
|
||||
.add(createTipLabel("\\b - ")).xy(7, 2)
|
||||
.add(createTipLabel("Backspace")).xy(8, 2)
|
||||
.build()
|
||||
|
||||
return JPanel(BorderLayout()).apply {
|
||||
add(panel, BorderLayout.CENTER)
|
||||
border = BorderFactory.createEmptyBorder(4, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTipLabel(text: String): JLabel {
|
||||
val label = JLabel(text)
|
||||
label.foreground = UIManager.getColor("textInactiveText")
|
||||
return label
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
146
src/main/kotlin/app/termora/snippet/SnippetTree.kt
Normal file
146
src/main/kotlin/app/termora/snippet/SnippetTree.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.OptionPane
|
||||
import app.termora.SimpleTree
|
||||
import app.termora.SimpleTreeNode
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.DropMode
|
||||
import javax.swing.JMenu
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class SnippetTree : SimpleTree() {
|
||||
override val model = SnippetTreeModel()
|
||||
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
super.setModel(model)
|
||||
isEditable = true
|
||||
dragEnabled = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
override fun showContextmenu(evt: MouseEvent) {
|
||||
val lastNode = getLastSelectedPathNode() ?: return
|
||||
val popupMenu = FlatPopupMenu()
|
||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||
val newSnippet = newMenu.add(I18n.getString("termora.snippet"))
|
||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||
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()
|
||||
|
||||
newFolder.addActionListener {
|
||||
val snippet = Snippet(
|
||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||
type = SnippetType.Folder,
|
||||
parentId = lastNode.data.id
|
||||
)
|
||||
snippetManager.addSnippet(snippet)
|
||||
newFolder(SnippetTreeNode(snippet))
|
||||
}
|
||||
newSnippet.addActionListener {
|
||||
val snippet = Snippet(
|
||||
name = I18n.getString("termora.snippet"),
|
||||
type = SnippetType.Snippet,
|
||||
parentId = lastNode.data.id
|
||||
)
|
||||
snippetManager.addSnippet(snippet)
|
||||
newFile(SnippetTreeNode(snippet))
|
||||
}
|
||||
|
||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||
refresh.addActionListener { refreshNode(lastNode) }
|
||||
expandAll.addActionListener {
|
||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
colspanAll.addActionListener {
|
||||
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
remove.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
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) {
|
||||
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
||||
model.removeNodeFromParent(c)
|
||||
// 将所有子孙也删除
|
||||
for (child in c.getAllChildren()) {
|
||||
snippetManager.addSnippet(
|
||||
child.data.copy(
|
||||
deleted = true,
|
||||
updateDate = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
rename.isEnabled = lastNode != model.root
|
||||
remove.isEnabled = rename.isEnabled
|
||||
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
|
||||
newSnippet.isEnabled = newFolder.isEnabled
|
||||
newMenu.isEnabled = newFolder.isEnabled
|
||||
refresh.isEnabled = newFolder.isEnabled
|
||||
|
||||
popupMenu.add(newMenu)
|
||||
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
public override fun getLastSelectedPathNode(): SnippetTreeNode? {
|
||||
return super.getLastSelectedPathNode() as? SnippetTreeNode
|
||||
}
|
||||
|
||||
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||
val n = node as? SnippetTreeNode ?: return
|
||||
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(n.data)
|
||||
model.nodeStructureChanged(n)
|
||||
}
|
||||
|
||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
val nNode = node as? SnippetTreeNode ?: return
|
||||
val nParent = parent as? SnippetTreeNode ?: return
|
||||
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(nNode.data)
|
||||
}
|
||||
|
||||
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {
|
||||
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<SnippetTreeNode>()
|
||||
}
|
||||
}
|
||||
65
src/main/kotlin/app/termora/snippet/SnippetTreeDialog.kt
Normal file
65
src/main/kotlin/app/termora/snippet/SnippetTreeDialog.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JScrollPane
|
||||
|
||||
class SnippetTreeDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val snippetTree = SnippetTree()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
var lastNode: SnippetTreeNode? = null
|
||||
|
||||
init {
|
||||
size = Dimension(360, 380)
|
||||
title = I18n.getString("termora.snippet.title")
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetTreeDialog.Tree.expansionState", TreeUtils.saveExpansionState(snippetTree))
|
||||
properties.putString("SnippetTreeDialog.Tree.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
val expansionState = properties.getString("SnippetTreeDialog.Tree.expansionState", StringUtils.EMPTY)
|
||||
if (expansionState.isNotBlank()) {
|
||||
TreeUtils.loadExpansionState(snippetTree, expansionState)
|
||||
}
|
||||
|
||||
val selectionRows = properties.getString("SnippetTreeDialog.Tree.selectionRows", StringUtils.EMPTY)
|
||||
if (selectionRows.isNotBlank()) {
|
||||
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return JScrollPane(snippetTree).apply { border = BorderFactory.createEmptyBorder(0, 6, 6, 6) }
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
lastNode = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val node = snippetTree.getLastSelectedPathNode() ?: return
|
||||
if (node.isFolder) return
|
||||
lastNode = node
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getSelectedNode(): SnippetTreeNode? {
|
||||
return lastNode
|
||||
}
|
||||
}
|
||||
73
src/main/kotlin/app/termora/snippet/SnippetTreeModel.kt
Normal file
73
src/main/kotlin/app/termora/snippet/SnippetTreeModel.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.SimpleTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class SnippetTreeModel : SimpleTreeModel<Snippet>(
|
||||
SnippetTreeNode(
|
||||
Snippet(
|
||||
id = "0",
|
||||
name = "全部片段",
|
||||
type = SnippetType.Folder
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
init {
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun getRoot(): SnippetTreeNode {
|
||||
return super.getRoot() as SnippetTreeNode
|
||||
}
|
||||
|
||||
override fun reload(parent: TreeNode?) {
|
||||
|
||||
if (parent !is SnippetTreeNode) {
|
||||
super.reload(parent)
|
||||
return
|
||||
}
|
||||
|
||||
parent.removeAllChildren()
|
||||
|
||||
val hosts = snippetManager.snippets()
|
||||
val nodes = linkedMapOf<String, SnippetTreeNode>()
|
||||
|
||||
// 遍历 Host 列表,构建树节点
|
||||
for (host in hosts) {
|
||||
val node = SnippetTreeNode(host)
|
||||
nodes[host.id] = node
|
||||
}
|
||||
|
||||
for (host in hosts) {
|
||||
val node = nodes[host.id] ?: continue
|
||||
if (host.parentId.isBlank()) continue
|
||||
val p = nodes[host.parentId] ?: continue
|
||||
p.add(node)
|
||||
}
|
||||
|
||||
for ((_, v) in nodes.entries) {
|
||||
if (parent.data.id == v.data.parentId) {
|
||||
parent.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
super.reload(parent)
|
||||
}
|
||||
|
||||
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
|
||||
super.insertNodeInto(newChild, parent, index)
|
||||
// 重置所有排序
|
||||
if (parent is SnippetTreeNode) {
|
||||
for ((i, c) in parent.children().toList().filterIsInstance<SnippetTreeNode>().withIndex()) {
|
||||
val sort = i.toLong()
|
||||
if (c.data.sort == sort) continue
|
||||
c.data = c.data.copy(sort = sort, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(c.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/app/termora/snippet/SnippetTreeNode.kt
Normal file
26
src/main/kotlin/app/termora/snippet/SnippetTreeNode.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.Icons
|
||||
import app.termora.SimpleTreeNode
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import javax.swing.Icon
|
||||
|
||||
class SnippetTreeNode(snippet: Snippet) : SimpleTreeNode<Snippet>(snippet) {
|
||||
|
||||
override val folderCount: Int
|
||||
get() = children().toList().count { if (it is SnippetTreeNode) it.data.type == SnippetType.Folder else false }
|
||||
override val id get() = data.id
|
||||
override val isFolder get() = data.type == SnippetType.Folder
|
||||
|
||||
override fun toString(): String {
|
||||
return data.name
|
||||
}
|
||||
|
||||
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||
return when (data.type) {
|
||||
SnippetType.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
else -> if (selected && hasFocus) Icons.codeSpan.dark else Icons.codeSpan
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,14 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
|
||||
decodeSnippets(it.content, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
|
||||
}
|
||||
@@ -84,6 +92,16 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
gistFiles.add(GistFile("Hosts", hostsContent))
|
||||
}
|
||||
|
||||
|
||||
// Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
val snippetsContent = encodeSnippets(key)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push encryptedSnippets: {}", snippetsContent)
|
||||
}
|
||||
gistFiles.add(GistFile("Snippets", snippetsContent))
|
||||
}
|
||||
|
||||
// KeyPairs
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keysContent = encodeKeys(key)
|
||||
|
||||
@@ -14,7 +14,8 @@ import app.termora.keymgr.KeyManager
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.macro.MacroManager
|
||||
import kotlinx.serialization.encodeToString
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.snippet.SnippetManager
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -32,6 +33,7 @@ abstract class SafetySyncer : Syncer {
|
||||
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
protected val macroManager get() = MacroManager.getInstance()
|
||||
protected val keymapManager get() = KeymapManager.getInstance()
|
||||
protected val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
protected fun decodeHosts(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
@@ -131,6 +133,61 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeSnippets(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
|
||||
val snippets = snippetManager.snippets().associateBy { it.id }
|
||||
|
||||
for (encryptedSnippet in encryptedSnippets) {
|
||||
val oldHost = snippets[encryptedSnippet.id]
|
||||
|
||||
// 如果一样,则无需配置
|
||||
if (oldHost != null) {
|
||||
if (oldHost.updateDate == encryptedSnippet.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// aes iv
|
||||
val iv = getIv(encryptedSnippet.id)
|
||||
val snippet = encryptedSnippet.copy(
|
||||
name = encryptedSnippet.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
parentId = encryptedSnippet.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
snippet = encryptedSnippet.snippet.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
)
|
||||
SwingUtilities.invokeLater { snippetManager.addSnippet(snippet) }
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Decode snippet: ${encryptedSnippet.id} failed. error: {}", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode hosts: {}", text)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun encodeSnippets(key: ByteArray): String {
|
||||
val snippets = mutableListOf<Snippet>()
|
||||
for (snippet in snippetManager.snippets()) {
|
||||
// aes iv
|
||||
val iv = ArrayUtils.subarray(snippet.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||
snippets.add(
|
||||
snippet.copy(
|
||||
name = snippet.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
snippet = snippet.snippet.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
parentId = snippet.parentId.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
)
|
||||
)
|
||||
}
|
||||
return ohMyJson.encodeToString(snippets)
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeKeys(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
|
||||
@@ -13,6 +13,7 @@ enum class SyncRange {
|
||||
KeywordHighlights,
|
||||
Macros,
|
||||
Keymap,
|
||||
Snippets,
|
||||
}
|
||||
|
||||
data class SyncConfig(
|
||||
|
||||
@@ -4,7 +4,6 @@ import app.termora.Application.ohMyJson
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.PBKDF2
|
||||
import app.termora.ResponseException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -37,28 +36,45 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||
|
||||
// decode hosts
|
||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||
decodeHosts(it, config)
|
||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||
decodeHosts(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode KeyPairs
|
||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeys(it, config)
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeys(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Highlights
|
||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeywordHighlights(it, config)
|
||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeywordHighlights(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Macros
|
||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||
decodeMacros(it, config)
|
||||
if (config.ranges.contains(SyncRange.Macros)) {
|
||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||
decodeMacros(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Keymaps
|
||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeymaps(it, config)
|
||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeymaps(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
json["Snippets"]?.jsonPrimitive?.content?.let {
|
||||
decodeSnippets(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
return GistResponse(config, emptyList())
|
||||
@@ -77,6 +93,15 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
put("Hosts", hostsContent)
|
||||
}
|
||||
|
||||
// Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
val snippetsContent = encodeSnippets(key)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push encryptedSnippets: {}", snippetsContent)
|
||||
}
|
||||
put("Snippets", snippetsContent)
|
||||
}
|
||||
|
||||
// KeyPairs
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keysContent = encodeKeys(key)
|
||||
|
||||
@@ -5,6 +5,8 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.snippet.SnippetAction
|
||||
import app.termora.snippet.SnippetTreeDialog
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||
@@ -95,7 +97,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (isVisible == true) {
|
||||
if (isVisible) {
|
||||
isVisible = false
|
||||
firePropertyChange("visible", true, false)
|
||||
}
|
||||
@@ -108,6 +110,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Snippet
|
||||
add(initSnippetActionButton())
|
||||
|
||||
// Nvidia 显卡信息
|
||||
add(initNvidiaSMIActionButton())
|
||||
|
||||
@@ -146,6 +151,24 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initSnippetActionButton(): JButton {
|
||||
val btn = JButton(Icons.codeSpan)
|
||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminal = tab.getData(DataProviders.Terminal) ?: return
|
||||
val dialog = SnippetTreeDialog(evt.window)
|
||||
dialog.setLocationRelativeTo(btn)
|
||||
dialog.setLocation(dialog.x, btn.locationOnScreen.y + height + 2)
|
||||
dialog.isVisible = true
|
||||
val node = dialog.getSelectedNode() ?: return
|
||||
SnippetAction.getInstance().runSnippet(node.data, terminal)
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initNvidiaSMIActionButton(): JButton {
|
||||
val btn = JButton(Icons.nvidia)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
|
||||
Reference in New Issue
Block a user