feat: support snippet (#321)

This commit is contained in:
hstyi
2025-02-27 16:48:25 +08:00
committed by GitHub
parent dcc96358f6
commit 483a7772f4
43 changed files with 1484 additions and 375 deletions

View File

@@ -5,6 +5,7 @@ import app.termora.highlight.KeywordHighlight
import app.termora.keymap.Keymap import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.snippet.Snippet
import app.termora.sync.SyncType import app.termora.sync.SyncType
import app.termora.terminal.CursorStyle import app.termora.terminal.CursorStyle
import jetbrains.exodus.bindings.StringBinding import jetbrains.exodus.bindings.StringBinding
@@ -12,7 +13,6 @@ import jetbrains.exodus.env.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -26,6 +26,7 @@ class Database private constructor(private val env: Environment) : Disposable {
companion object { companion object {
private const val KEYMAP_STORE = "Keymap" private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host" private const val HOST_STORE = "Host"
private const val SNIPPET_STORE = "Snippet"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight" private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro" private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair" 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() { fun removeAllKeyPair() {
env.executeInTransaction { tx -> env.executeInTransaction { tx ->
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, 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 { env.executeInTransaction {
delete(it, HOST_STORE, id) put(it, SNIPPET_STORE, snippet.id, text)
if (log.isDebugEnabled) { 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> { fun getKeywordHighlights(): Collection<KeywordHighlight> {
return env.computeInTransaction { tx -> return env.computeInTransaction { tx ->
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value -> 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 rangeHosts by BooleanPropertyDelegate(true)
var rangeKeyPairs by BooleanPropertyDelegate(true) var rangeKeyPairs by BooleanPropertyDelegate(true)
var rangeSnippets by BooleanPropertyDelegate(true)
var rangeKeywordHighlights by BooleanPropertyDelegate(true) var rangeKeywordHighlights by BooleanPropertyDelegate(true)
var rangeMacros by BooleanPropertyDelegate(true) var rangeMacros by BooleanPropertyDelegate(true)
var rangeKeymap by BooleanPropertyDelegate(true) var rangeKeymap by BooleanPropertyDelegate(true)

View File

@@ -41,7 +41,7 @@ class FilterableHostTreeModel(
continue continue
} }
if (c.host.protocol != Protocol.Folder) { if (c.data.protocol != Protocol.Folder) {
if (filters.isNotEmpty() && filters.none { it.apply(c) }) { if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
continue continue
} }

View File

@@ -1,17 +1,29 @@
package app.termora 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 import javax.swing.tree.TreeNode
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) { class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
companion object { companion object {
private val hostManager get() = HostManager.getInstance() 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] 否则下次取出时可能时缓存的 * 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
*/ */
var host: Host override var data: Host
get() { get() {
val cacheHost = hostManager.getHost((userObject as Host).id) val cacheHost = hostManager.getHost((userObject as Host).id)
val myHost = userObject as Host val myHost = userObject as Host
@@ -22,22 +34,23 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
} }
set(value) = setUserObject(value) set(value) = setUserObject(value)
val folderCount override val folderCount
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false } get() = children().toList().count { if (it is HostTreeNode) it.data.protocol == Protocol.Folder else false }
override fun getParent(): HostTreeNode? { override fun getParent(): HostTreeNode? {
return super.getParent() as HostTreeNode? return super.getParent() as HostTreeNode?
} }
fun getAllChildren(): List<HostTreeNode> { override fun getAllChildren(): List<HostTreeNode> {
val children = mutableListOf<HostTreeNode>() return super.getAllChildren().filterIsInstance<HostTreeNode>()
for (child in children()) { }
if (child is HostTreeNode) {
children.add(child) override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
children.addAll(child.getAllChildren()) 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> { 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()) { private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
for (child in oldNode.childrenNode()) { 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 val newChildNode = child.clone() as HostTreeNode
deepClone(newChildNode, child, scopes) deepClone(newChildNode, child, scopes)
newNode.add(newChildNode) newNode.add(newChildNode)
@@ -65,7 +78,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
} }
override fun clone(): Any { override fun clone(): Any {
val newNode = HostTreeNode(host) val newNode = HostTreeNode(data)
newNode.children = null newNode.children = null
newNode.parent = null newNode.parent = null
return newNode return newNode
@@ -74,7 +87,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
override fun isNodeChild(aNode: TreeNode?): Boolean { override fun isNodeChild(aNode: TreeNode?): Boolean {
if (aNode is HostTreeNode) { if (aNode is HostTreeNode) {
for (node in childrenNode()) { for (node in childrenNode()) {
if (node.host == aNode.host) { if (node.data == aNode.data) {
return true return true
} }
} }
@@ -88,10 +101,10 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
other as HostTreeNode other as HostTreeNode
return host == other.host return data == other.data
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return host.hashCode() return data.hashCode()
} }
} }

View File

@@ -94,6 +94,9 @@ object Icons {
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") } 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 right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_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 fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") } val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
val applyNotConflictsLeft by lazy { val applyNotConflictsLeft by lazy {

View File

@@ -5,8 +5,6 @@ import app.termora.actions.AnActionEvent
import app.termora.actions.OpenHostAction import app.termora.actions.OpenHostAction
import app.termora.transport.SFTPAction import app.termora.transport.SFTPAction
import com.formdev.flatlaf.extras.components.FlatPopupMenu 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.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.apache.commons.csv.CSVFormat 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.apache.commons.lang3.exception.ExceptionUtils
import org.ini4j.Ini import org.ini4j.Ini
import org.ini4j.Reg import org.ini4j.Reg
import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import java.awt.Component 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.awt.event.*
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.function.Function import java.util.function.Function
import javax.swing.* 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.filechooser.FileNameExtensionFilter
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel import javax.swing.tree.TreeSelectionModel
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
import kotlin.math.min
class NewHostTree : JXTree() { class NewHostTree : SimpleTree() {
companion object { companion object {
private val log = LoggerFactory.getLogger(NewHostTree::class.java) private val log = LoggerFactory.getLogger(NewHostTree::class.java)
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol") 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 hostManager get() = HostManager.getInstance()
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
@@ -64,7 +50,7 @@ class NewHostTree : JXTree() {
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean() get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
set(value) = properties.putString("HostTree.showMoreInfo", value.toString()) 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 isRootVisible = true
dropMode = DropMode.ON_OR_INSERT dropMode = DropMode.ON_OR_INSERT
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
editor.preferredSize = Dimension(220, 0)
// renderer // renderer
setCellRenderer(object : DefaultXTreeCellRenderer() { setCellRenderer(object : DefaultXTreeCellRenderer() {
@@ -135,74 +120,16 @@ class NewHostTree : JXTree() {
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus) val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
icon = when (host.protocol) { icon = node.getIcon(sel, expanded, hasFocus)
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 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() { private fun initEvents() {
// 右键选中 // double click
addMouseListener(object : MouseAdapter() { 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) { override fun mouseClicked(e: MouseEvent) {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
@@ -216,7 +143,7 @@ class NewHostTree : JXTree() {
addKeyListener(object : KeyAdapter() { addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) { override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) { if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
val nodes = getSelectionHostTreeNodes(false) val nodes = getSelectionSimpleTreeNodes()
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) { if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
val path = TreePath(model.getPathToRoot(nodes.first())) val path = TreePath(model.getPathToRoot(nodes.first()))
if (isExpanded(path)) { if (isExpanded(path)) {
@@ -225,7 +152,7 @@ class NewHostTree : JXTree() {
expandPath(path) expandPath(path)
} }
} else { } else {
for (node in getSelectionHostTreeNodes(true)) { for (node in getSelectionSimpleTreeNodes(true)) {
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e)) 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 val lastNode = lastSelectedPathComponent
if (lastNode !is HostTreeNode) return if (lastNode !is HostTreeNode) return
val nodes = getSelectionSimpleTreeNodes()
val fullNodes = getSelectionSimpleTreeNodes(true)
val lastNodeParent = lastNode.parent ?: model.root val lastNodeParent = lastNode.parent ?: model.root
val lastHost = lastNode.host val lastHost = lastNode.host
@@ -443,7 +241,6 @@ class NewHostTree : JXTree() {
} }
remove.addActionListener(object : ActionListener { remove.addActionListener(object : ActionListener {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val nodes = getSelectionHostTreeNodes()
if (nodes.isEmpty()) return if (nodes.isEmpty()) return
if (OptionPane.showConfirmDialog( if (OptionPane.showConfirmDialog(
SwingUtilities.getWindowAncestor(tree), SwingUtilities.getWindowAncestor(tree),
@@ -470,7 +267,7 @@ class NewHostTree : JXTree() {
} }
}) })
copy.addActionListener { copy.addActionListener {
for (c in getSelectionHostTreeNodes()) { for (c in nodes) {
val p = c.parent ?: continue val p = c.parent ?: continue
val newNode = copyNode(c, p.host.id) val newNode = copyNode(c, p.host.id)
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1) model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
@@ -479,12 +276,12 @@ class NewHostTree : JXTree() {
} }
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) } rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
expandAll.addActionListener { expandAll.addActionListener {
for (node in getSelectionHostTreeNodes(true)) { for (node in fullNodes) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(model.getPathToRoot(node)))
} }
} }
colspanAll.addActionListener { colspanAll.addActionListener {
for (node in getSelectionHostTreeNodes(true).reversed()) { for (node in fullNodes.reversed()) {
collapsePath(TreePath(model.getPathToRoot(node))) collapsePath(TreePath(model.getPathToRoot(node)))
} }
} }
@@ -512,29 +309,10 @@ class NewHostTree : JXTree() {
model.nodeStructureChanged(lastNode) model.nodeStructureChanged(lastNode)
} }
}) })
refresh.addActionListener { refresh.addActionListener { refreshNode(lastNode) }
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)))
}
}
}
newMenu.isEnabled = lastHost.protocol == Protocol.Folder newMenu.isEnabled = lastHost.protocol == Protocol.Folder
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root } remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
copy.isEnabled = remove.isEnabled copy.isEnabled = remove.isEnabled
rename.isEnabled = remove.isEnabled rename.isEnabled = remove.isEnabled
property.isEnabled = lastHost.protocol != Protocol.Folder property.isEnabled = lastHost.protocol != Protocol.Folder
@@ -542,28 +320,27 @@ class NewHostTree : JXTree() {
importMenu.isEnabled = lastHost.protocol == Protocol.Folder importMenu.isEnabled = lastHost.protocol == Protocol.Folder
// 如果选中了 SSH 服务器,那么才启用 // 如果选中了 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 openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled } openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
popupMenu.show(this, evt.x, evt.y)
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)
} }
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( private fun copyNode(
node: HostTreeNode, node: HostTreeNode,
parentId: String, parentId: String,
@@ -600,30 +377,14 @@ class NewHostTree : JXTree() {
} }
/** override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
* 包含孙子 return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
*/
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) { private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
assertEventDispatchThread() 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 if (nodes.isEmpty()) return
val source = if (openInNewWindow) val source = if (openInNewWindow)
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true } TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
@@ -632,7 +393,7 @@ class NewHostTree : JXTree() {
} }
private fun openWithSFTP(evt: EventObject) { 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 if (nodes.isEmpty()) return
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: 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) { 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 if (nodes.isEmpty()) return
for (host in nodes) { for (host in nodes) {
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt)) openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
@@ -1106,28 +867,5 @@ class NewHostTree : JXTree() {
electerm, 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)
}
}
} }

View File

@@ -60,7 +60,7 @@ class NewHostTreeDialog(
} }
override fun doOKAction() { override fun doOKAction() {
hosts = tree.getSelectionHostTreeNodes(true) hosts = tree.getSelectionSimpleTreeNodes(true)
.filter { filter.apply(it) } .filter { filter.apply(it) }
.map { it.host } .map { it.host }

View File

@@ -1,12 +1,11 @@
package app.termora package app.termora
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode import javax.swing.tree.TreeNode
class NewHostTreeModel : DefaultTreeModel( class NewHostTreeModel : SimpleTreeModel<Host>(
HostTreeNode( HostTreeNode(
Host( Host(
id = "0", id = "0",

View File

@@ -15,6 +15,8 @@ import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import app.termora.native.FileChooser import app.termora.native.FileChooser
import app.termora.snippet.Snippet
import app.termora.snippet.SnippetManager
import app.termora.sync.SyncConfig import app.termora.sync.SyncConfig
import app.termora.sync.SyncRange import app.termora.sync.SyncRange
import app.termora.sync.SyncType import app.termora.sync.SyncType
@@ -67,6 +69,7 @@ class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane) private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
private val database get() = Database.getDatabase() private val database get() = Database.getDatabase()
private val hostManager get() = HostManager.getInstance() private val hostManager get() = HostManager.getInstance()
private val snippetManager get() = SnippetManager.getInstance()
private val keymapManager get() = KeymapManager.getInstance() private val keymapManager get() = KeymapManager.getInstance()
private val macroManager get() = MacroManager.getInstance() private val macroManager get() = MacroManager.getInstance()
private val actionManager get() = ActionManager.getInstance() private val actionManager get() = ActionManager.getInstance()
@@ -561,6 +564,7 @@ class SettingsOptionsPane : OptionsPane() {
val sync get() = database.sync val sync get() = database.sync
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts")) val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys")) 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 keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro")) val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap")) val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
@@ -665,6 +669,7 @@ class SettingsOptionsPane : OptionsPane() {
keysCheckBox.addActionListener { refreshButtons() } keysCheckBox.addActionListener { refreshButtons() }
hostsCheckBox.addActionListener { refreshButtons() } hostsCheckBox.addActionListener { refreshButtons() }
snippetsCheckBox.addActionListener { refreshButtons() }
keywordHighlightsCheckBox.addActionListener { refreshButtons() } keywordHighlightsCheckBox.addActionListener { refreshButtons() }
} }
@@ -672,6 +677,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun refreshButtons() { private fun refreshButtons() {
sync.rangeKeyPairs = keysCheckBox.isSelected sync.rangeKeyPairs = keysCheckBox.isSelected
sync.rangeHosts = hostsCheckBox.isSelected sync.rangeHosts = hostsCheckBox.isSelected
sync.rangeSnippets = snippetsCheckBox.isSelected
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.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)) { if (ranges.contains(SyncRange.KeyPairs)) {
val keyPairs = json["keyPairs"] val keyPairs = json["keyPairs"]
if (keyPairs is JsonArray) { if (keyPairs is JsonArray) {
@@ -909,6 +926,9 @@ class SettingsOptionsPane : OptionsPane() {
if (syncConfig.ranges.contains(SyncRange.Hosts)) { if (syncConfig.ranges.contains(SyncRange.Hosts)) {
put("hosts", ohMyJson.encodeToJsonElement(hostManager.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)) { if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs())) put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
} }
@@ -978,6 +998,9 @@ class SettingsOptionsPane : OptionsPane() {
if (keymapCheckBox.isSelected) { if (keymapCheckBox.isSelected) {
range.add(SyncRange.Keymap) range.add(SyncRange.Keymap)
} }
if (snippetsCheckBox.isSelected) {
range.add(SyncRange.Snippets)
}
return SyncConfig( return SyncConfig(
type = typeComboBox.selectedItem as SyncType, type = typeComboBox.selectedItem as SyncType,
token = String(tokenTextField.password), token = String(tokenTextField.password),
@@ -1054,6 +1077,7 @@ class SettingsOptionsPane : OptionsPane() {
keymapCheckBox.isEnabled = false keymapCheckBox.isEnabled = false
keywordHighlightsCheckBox.isEnabled = false keywordHighlightsCheckBox.isEnabled = false
hostsCheckBox.isEnabled = false hostsCheckBox.isEnabled = false
snippetsCheckBox.isEnabled = false
domainTextField.isEnabled = false domainTextField.isEnabled = false
if (push) { if (push) {
@@ -1083,6 +1107,7 @@ class SettingsOptionsPane : OptionsPane() {
uploadConfigButton.isEnabled = true uploadConfigButton.isEnabled = true
keysCheckBox.isEnabled = true keysCheckBox.isEnabled = true
hostsCheckBox.isEnabled = true hostsCheckBox.isEnabled = true
snippetsCheckBox.isEnabled = true
typeComboBox.isEnabled = true typeComboBox.isEnabled = true
macrosCheckBox.isEnabled = true macrosCheckBox.isEnabled = true
keymapCheckBox.isEnabled = true keymapCheckBox.isEnabled = true
@@ -1144,12 +1169,14 @@ class SettingsOptionsPane : OptionsPane() {
typeComboBox.addItem(SyncType.WebDAV) typeComboBox.addItem(SyncType.WebDAV)
hostsCheckBox.isFocusable = false hostsCheckBox.isFocusable = false
snippetsCheckBox.isFocusable = false
keysCheckBox.isFocusable = false keysCheckBox.isFocusable = false
keywordHighlightsCheckBox.isFocusable = false keywordHighlightsCheckBox.isFocusable = false
macrosCheckBox.isFocusable = false macrosCheckBox.isFocusable = false
keymapCheckBox.isFocusable = false keymapCheckBox.isFocusable = false
hostsCheckBox.isSelected = sync.rangeHosts hostsCheckBox.isSelected = sync.rangeHosts
snippetsCheckBox.isSelected = sync.rangeSnippets
keysCheckBox.isSelected = sync.rangeKeyPairs keysCheckBox.isSelected = sync.rangeKeyPairs
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
macrosCheckBox.isSelected = sync.rangeMacros macrosCheckBox.isSelected = sync.rangeMacros
@@ -1236,7 +1263,7 @@ class SettingsOptionsPane : OptionsPane() {
.layout( .layout(
FormLayout( FormLayout(
"left:pref, $formMargin, left:pref, $formMargin, left:pref", "left:pref, $formMargin, left:pref, $formMargin, left:pref",
"pref, $formMargin, pref" "pref, 2dlu, pref"
) )
) )
.add(hostsCheckBox).xy(1, 1) .add(hostsCheckBox).xy(1, 1)
@@ -1244,6 +1271,7 @@ class SettingsOptionsPane : OptionsPane() {
.add(keywordHighlightsCheckBox).xy(5, 1) .add(keywordHighlightsCheckBox).xy(5, 1)
.add(macrosCheckBox).xy(1, 3) .add(macrosCheckBox).xy(1, 3)
.add(keymapCheckBox).xy(3, 3) .add(keymapCheckBox).xy(3, 3)
.add(snippetsCheckBox).xy(5, 3)
.build() .build()
var rows = 1 var rows = 1
@@ -1612,13 +1640,15 @@ class SettingsOptionsPane : OptionsPane() {
val hosts = hostManager.hosts() val hosts = hostManager.hosts()
val keyPairs = keyManager.getOhKeyPairs() val keyPairs = keyManager.getOhKeyPairs()
val snippets = snippetManager.snippets()
// 获取到安全的属性,如果设置密码那表示之前并未加密 // 获取到安全的属性,如果设置密码那表示之前并未加密
// 这里取出来之后重新存储加密 // 这里取出来之后重新存储加密
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) } val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
val key = doorman.work(passwordTextField.password) val key = doorman.work(passwordTextField.password)
hosts.forEach { hostManager.addHost(it) } hosts.forEach { hostManager.addHost(it) }
snippets.forEach { snippetManager.addSnippet(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

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

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

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

View File

@@ -1,10 +1,11 @@
package app.termora package app.termora
import app.termora.actions.DataProvider
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.Icon import javax.swing.Icon
import javax.swing.JComponent import javax.swing.JComponent
interface TerminalTab : Disposable { interface TerminalTab : Disposable, DataProvider {
/** /**
* 标题 * 标题

View File

@@ -6,6 +6,7 @@ import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction import app.termora.actions.SettingsAction
import app.termora.findeverywhere.FindEverywhereAction import app.termora.findeverywhere.FindEverywhereAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.WindowDecorations import com.jetbrains.WindowDecorations
@@ -42,6 +43,7 @@ class TermoraToolBar(
*/ */
fun getAllActions(): List<ToolBarAction> { fun getAllActions(): List<ToolBarAction> {
return listOf( return listOf(
ToolBarAction(SnippetAction.SNIPPET, true),
ToolBarAction(Actions.SFTP, true), ToolBarAction(Actions.SFTP, true),
ToolBarAction(Actions.TERMINAL_LOGGER, true), ToolBarAction(Actions.TERMINAL_LOGGER, true),
ToolBarAction(Actions.MACRO, true), ToolBarAction(Actions.MACRO, true),

View File

@@ -1,8 +1,8 @@
package app.termora package app.termora
import org.apache.commons.lang3.StringUtils
import javax.swing.JTree import javax.swing.JTree
import javax.swing.tree.TreeModel import javax.swing.tree.TreeModel
import javax.swing.tree.TreeNode
object TreeUtils { object TreeUtils {
/** /**
@@ -31,16 +31,6 @@ object TreeUtils {
return nodes 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 { fun saveExpansionState(tree: JTree): String {
val rows = mutableListOf<Int>() val rows = mutableListOf<Int>()
for (i in 0 until tree.rowCount) { for (i in 0 until tree.rowCount) {
@@ -63,15 +53,15 @@ object TreeUtils {
} }
} }
fun expandAll(tree: JTree) { fun saveSelectionRows(tree: JTree): String {
var j = tree.rowCount return tree.selectionRows?.joinToString(",") ?: StringUtils.EMPTY
var i = 0 }
while (i < j) {
tree.expandRow(i) fun loadSelectionRows(tree: JTree, state: String) {
i += 1 if (state.isBlank()) return
j = tree.rowCount for (row in state.split(",").mapNotNull { it.toIntOrNull() }) {
tree.addSelectionRow(row)
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import app.termora.findeverywhere.FindEverywhereAction
import app.termora.highlight.KeywordHighlightAction import app.termora.highlight.KeywordHighlightAction
import app.termora.keymgr.KeyManagerAction import app.termora.keymgr.KeyManagerAction
import app.termora.macro.MacroAction import app.termora.macro.MacroAction
import app.termora.snippet.SnippetAction
import app.termora.tlog.TerminalLoggerAction import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction import app.termora.transport.SFTPAction
import javax.swing.Action import javax.swing.Action
@@ -34,6 +35,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction()) addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, SFTPAction()) addAction(Actions.SFTP, SFTPAction())
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction()) addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
addAction(Actions.MACRO, MacroAction()) addAction(Actions.MACRO, MacroAction())
addAction(Actions.KEY_MANAGER, KeyManagerAction()) addAction(Actions.KEY_MANAGER, KeyManagerAction())

View File

@@ -17,5 +17,5 @@ interface DataProvider {
/** /**
* 数据提供 * 数据提供
*/ */
fun <T : Any> getData(dataKey: DataKey<T>): T? fun <T : Any> getData(dataKey: DataKey<T>): T? = null
} }

View File

@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
object DataProviders { object DataProviders {
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class) val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
val Terminal = DataKey(app.termora.terminal.Terminal::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 TabbedPane = DataKey(app.termora.MyTabbedPane::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class) val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)

View File

@@ -6,6 +6,7 @@ import org.jdesktop.swingx.action.AbstractActionExt
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import javax.swing.Action import javax.swing.Action
import javax.swing.Icon import javax.swing.Icon
import javax.swing.SwingUtilities
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult { open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
private val isState: Boolean private val isState: Boolean
@@ -26,7 +27,7 @@ open class ActionFindEverywhereResult(private val action: Action) : FindEverywhe
if (isState) { if (isState) {
action.putValue(Action.SELECTED_KEY, !isSelected) action.putValue(Action.SELECTED_KEY, !isSelected)
} }
action.actionPerformed(e) SwingUtilities.invokeLater { action.actionPerformed(e) }
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {

View File

@@ -5,6 +5,7 @@ import app.termora.I18n
import app.termora.Icons import app.termora.Icons
import app.termora.actions.NewHostAction import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction import app.termora.actions.OpenLocalTerminalAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import javax.swing.Icon import javax.swing.Icon
@@ -21,6 +22,11 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
list.add(ActionFindEverywhereResult(it)) list.add(ActionFindEverywhereResult(it))
} }
// Snippet
actionManager.getAction(SnippetAction.SNIPPET)?.let {
list.add(ActionFindEverywhereResult(it))
}
// SFTP // SFTP
actionManager.getAction(Actions.SFTP)?.let { actionManager.getAction(Actions.SFTP)?.let {
list.add(ActionFindEverywhereResult(it)) list.add(ActionFindEverywhereResult(it))

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

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

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

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

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

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

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

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

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

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

View File

@@ -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) { if (log.isInfoEnabled) {
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled") log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
} }
@@ -84,6 +92,16 @@ abstract class GitSyncer : SafetySyncer() {
gistFiles.add(GistFile("Hosts", hostsContent)) 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 // KeyPairs
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
val keysContent = encodeKeys(key) val keysContent = encodeKeys(key)

View File

@@ -14,7 +14,8 @@ import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.macro.MacroManager 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 kotlinx.serialization.json.JsonObject
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -32,6 +33,7 @@ abstract class SafetySyncer : Syncer {
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
protected val macroManager get() = MacroManager.getInstance() protected val macroManager get() = MacroManager.getInstance()
protected val keymapManager get() = KeymapManager.getInstance() protected val keymapManager get() = KeymapManager.getInstance()
protected val snippetManager get() = SnippetManager.getInstance()
protected fun decodeHosts(text: String, config: SyncConfig) { protected fun decodeHosts(text: String, config: SyncConfig) {
// aes key // 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) { protected fun decodeKeys(text: String, config: SyncConfig) {
// aes key // aes key
val key = getKey(config) val key = getKey(config)

View File

@@ -13,6 +13,7 @@ enum class SyncRange {
KeywordHighlights, KeywordHighlights,
Macros, Macros,
Keymap, Keymap,
Snippets,
} }
data class SyncConfig( data class SyncConfig(

View File

@@ -4,7 +4,6 @@ import app.termora.Application.ohMyJson
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.PBKDF2 import app.termora.PBKDF2
import app.termora.ResponseException import app.termora.ResponseException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@@ -37,28 +36,45 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
val json = ohMyJson.decodeFromString<JsonObject>(text) val json = ohMyJson.decodeFromString<JsonObject>(text)
// decode hosts // decode hosts
json["Hosts"]?.jsonPrimitive?.content?.let { if (config.ranges.contains(SyncRange.Hosts)) {
decodeHosts(it, config) json["Hosts"]?.jsonPrimitive?.content?.let {
decodeHosts(it, config)
}
} }
// decode KeyPairs // decode KeyPairs
json["KeyPairs"]?.jsonPrimitive?.content?.let { if (config.ranges.contains(SyncRange.KeyPairs)) {
decodeKeys(it, config) json["KeyPairs"]?.jsonPrimitive?.content?.let {
decodeKeys(it, config)
}
} }
// decode Highlights // decode Highlights
json["KeywordHighlights"]?.jsonPrimitive?.content?.let { if (config.ranges.contains(SyncRange.KeywordHighlights)) {
decodeKeywordHighlights(it, config) json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
decodeKeywordHighlights(it, config)
}
} }
// decode Macros // decode Macros
json["Macros"]?.jsonPrimitive?.content?.let { if (config.ranges.contains(SyncRange.Macros)) {
decodeMacros(it, config) json["Macros"]?.jsonPrimitive?.content?.let {
decodeMacros(it, config)
}
} }
// decode Keymaps // decode Keymaps
json["Keymaps"]?.jsonPrimitive?.content?.let { if (config.ranges.contains(SyncRange.Keymap)) {
decodeKeymaps(it, config) 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()) return GistResponse(config, emptyList())
@@ -77,6 +93,15 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
put("Hosts", hostsContent) 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 // KeyPairs
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
val keysContent = encodeKeys(key) val keysContent = encodeKeys(key)

View File

@@ -5,6 +5,8 @@ import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.snippet.SnippetAction
import app.termora.snippet.SnippetTreeDialog
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
import app.termora.terminal.panel.vw.SystemInformationVisualWindow import app.termora.terminal.panel.vw.SystemInformationVisualWindow
@@ -95,7 +97,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
} }
} }
if (isVisible == true) { if (isVisible) {
isVisible = false isVisible = false
firePropertyChange("visible", true, false) firePropertyChange("visible", true, false)
} }
@@ -108,6 +110,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// 服务器信息 // 服务器信息
add(initServerInfoActionButton()) add(initServerInfoActionButton())
// Snippet
add(initSnippetActionButton())
// Nvidia 显卡信息 // Nvidia 显卡信息
add(initNvidiaSMIActionButton()) add(initNvidiaSMIActionButton())
@@ -146,6 +151,24 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
return btn 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 { private fun initNvidiaSMIActionButton(): JButton {
val btn = JButton(Icons.nvidia) val btn = JButton(Icons.nvidia)
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi") btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")

View File

@@ -251,6 +251,10 @@ termora.macro.playback=Playback
termora.macro.manager=Manage Macros termora.macro.manager=Manage Macros
termora.macro.run=Run termora.macro.run=Run
# Snippets
termora.snippet=Snippet
termora.snippet.title=Snippets
# Tools # Tools
termora.tools.multiple=Send commands to multiple sessions termora.tools.multiple=Send commands to multiple sessions

View File

@@ -245,6 +245,11 @@ termora.macro.manager=管理宏
termora.macro.run=运行 termora.macro.run=运行
# Snippets
termora.snippet=片段
termora.snippet.title=代码片段
# Transport # Transport
termora.transport.local=本机 termora.transport.local=本机

View File

@@ -239,6 +239,13 @@ termora.macro.playback=回放
termora.macro.manager=管理宏 termora.macro.manager=管理宏
termora.macro.run=運行 termora.macro.run=運行
# Snippets
termora.snippet=片段
termora.snippet.title=程式碼片段
# Transport # Transport
termora.transport.local=本機 termora.transport.local=本機
termora.transport.parent-folder=父資料夾 termora.transport.parent-folder=父資料夾

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 1H7.91421C7.51639 1 7.13486 1.15803 6.85355 1.43934L3.43934 4.85355C3.15804 5.13486 3 5.51639 3 5.91421V13C3 14.1046 3.89543 15 5 15H11C12.1046 15 13 14.1046 13 13V3C13 1.89543 12.1046 1 11 1ZM4 13C4 13.5523 4.44772 14 5 14H11C11.5523 14 12 13.5523 12 13V3C12 2.44772 11.5523 2 11 2H8V4.5C8 5.32843 7.32843 6 6.5 6H4V13ZM4.70711 5L7 2.70711V4.5C7 4.77614 6.77614 5 6.5 5H4.70711Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 1H7.91421C7.51639 1 7.13486 1.15803 6.85355 1.43934L3.43934 4.85355C3.15804 5.13486 3 5.51639 3 5.91421V13C3 14.1046 3.89543 15 5 15H11C12.1046 15 13 14.1046 13 13V3C13 1.89543 12.1046 1 11 1ZM4 13C4 13.5523 4.44772 14 5 14H11C11.5523 14 12 13.5523 12 13V3C12 2.44772 11.5523 2 11 2H8V4.5C8 5.32843 7.32843 6 6.5 6H4V13ZM4.70711 5L7 2.70711V4.5C7 4.77614 6.77614 5 6.5 5H4.70711Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.85355 4.35355C6.04882 4.15829 6.04882 3.84171 5.85355 3.64645C5.65829 3.45118 5.34171 3.45118 5.14645 3.64645L1.14645 7.64645C0.951184 7.84171 0.951184 8.15829 1.14645 8.35355L5.14645 12.3536C5.34171 12.5488 5.65829 12.5488 5.85355 12.3536C6.04882 12.1583 6.04882 11.8417 5.85355 11.6464L2.20711 8L5.85355 4.35355Z" fill="#6C707E"/>
<path d="M10.8536 3.64645C10.6583 3.45118 10.3417 3.45118 10.1464 3.64645C9.95118 3.84171 9.95118 4.15829 10.1464 4.35355L13.7929 8L10.1464 11.6464C9.95118 11.8417 9.95118 12.1583 10.1464 12.3536C10.3417 12.5488 10.6583 12.5488 10.8536 12.3536L14.8536 8.35355C15.0488 8.15829 15.0488 7.84171 14.8536 7.64645L10.8536 3.64645Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.85355 4.35355C6.04882 4.15829 6.04882 3.84171 5.85355 3.64645C5.65829 3.45118 5.34171 3.45118 5.14645 3.64645L1.14645 7.64645C0.951184 7.84171 0.951184 8.15829 1.14645 8.35355L5.14645 12.3536C5.34171 12.5488 5.65829 12.5488 5.85355 12.3536C6.04882 12.1583 6.04882 11.8417 5.85355 11.6464L2.20711 8L5.85355 4.35355Z" fill="#CED0D6"/>
<path d="M10.8536 3.64645C10.6583 3.45118 10.3417 3.45118 10.1464 3.64645C9.95118 3.84171 9.95118 4.15829 10.1464 4.35355L13.7929 8L10.1464 11.6464C9.95118 11.8417 9.95118 12.1583 10.1464 12.3536C10.3417 12.5488 10.6583 12.5488 10.8536 12.3536L14.8536 8.35355C15.0488 8.15829 15.0488 7.84171 14.8536 7.64645L10.8536 3.64645Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.98755 9.77751C3.99755 9.39251 3.87005 9.08501 3.60505 8.85501C3.34005 8.62501 2.98255 8.51001 2.53255 8.51001H2V7.43001H2.53255C2.98255 7.43001 3.34005 7.31501 3.60505 7.08501C3.87005 6.85501 3.99755 6.54751 3.98755 6.16251L3.95755 4.58C3.94755 4.06 4.05505 3.605 4.28005 3.215C4.51005 2.825 4.83755 2.525 5.26255 2.315C5.68755 2.105 6.18755 2 6.76255 2H7.5V3.005H6.77005C6.25005 3.005 5.83755 3.15 5.53255 3.44C5.23255 3.725 5.08505 4.1125 5.09005 4.6025L5.12005 6.17001C5.13005 6.68001 4.99255 7.09751 4.70755 7.42251C4.42255 7.74251 4.03755 7.92001 3.55255 7.95501C4.03755 8.04501 4.42255 8.25501 4.70755 8.58501C4.99255 8.91001 5.13005 9.30501 5.12005 9.77001L5.09005 11.5475C5.08005 11.9875 5.21255 12.3375 5.48755 12.5975C5.76755 12.8625 6.14505 12.995 6.62005 12.995H7.5V14H6.61255C6.07755 14 5.60755 13.9 5.20255 13.7C4.80255 13.505 4.49255 13.2225 4.27255 12.8525C4.05755 12.4875 3.95255 12.06 3.95755 11.57L3.98755 9.77751Z" fill="#6C707E"/>
<path d="M12.0145 9.77751C12.0045 9.39251 12.132 9.08501 12.397 8.85501C12.662 8.62501 13.0195 8.51001 13.4695 8.51001H14V7.43001H13.4695C13.0195 7.43001 12.662 7.31501 12.397 7.08501C12.132 6.85501 12.0045 6.54751 12.0145 6.16251L12.0445 4.58C12.0545 4.06 11.9445 3.605 11.7145 3.215C11.4895 2.825 11.1645 2.525 10.7395 2.315C10.3145 2.105 9.81455 2 9.23955 2H8.5V3.005H9.23205C9.75205 3.005 10.162 3.15 10.462 3.44C10.767 3.725 10.917 4.1125 10.912 4.6025L10.882 6.17001C10.872 6.68001 11.0095 7.09751 11.2945 7.42251C11.5795 7.74251 11.9645 7.92001 12.4495 7.95501C11.9645 8.04501 11.5795 8.25501 11.2945 8.58501C11.0095 8.91001 10.872 9.30501 10.882 9.77001L10.912 11.5475C10.922 11.9875 10.787 12.3375 10.507 12.5975C10.232 12.8625 9.85705 12.995 9.38205 12.995H8.5V14H9.38955C9.92455 14 10.392 13.9 10.792 13.7C11.197 13.505 11.507 13.2225 11.722 12.8525C11.942 12.4875 12.0495 12.06 12.0445 11.57L12.0145 9.77751Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.98755 9.77751C3.99755 9.39251 3.87005 9.08501 3.60505 8.85501C3.34005 8.62501 2.98255 8.51001 2.53255 8.51001H2V7.43001H2.53255C2.98255 7.43001 3.34005 7.31501 3.60505 7.08501C3.87005 6.85501 3.99755 6.54751 3.98755 6.16251L3.95755 4.58C3.94755 4.06 4.05505 3.605 4.28005 3.215C4.51005 2.825 4.83755 2.525 5.26255 2.315C5.68755 2.105 6.18755 2 6.76255 2H7.5V3.005H6.77005C6.25005 3.005 5.83755 3.15 5.53255 3.44C5.23255 3.725 5.08505 4.1125 5.09005 4.6025L5.12005 6.17001C5.13005 6.68001 4.99255 7.09751 4.70755 7.42251C4.42255 7.74251 4.03755 7.92001 3.55255 7.95501C4.03755 8.04501 4.42255 8.25501 4.70755 8.58501C4.99255 8.91001 5.13005 9.30501 5.12005 9.77001L5.09005 11.5475C5.08005 11.9875 5.21255 12.3375 5.48755 12.5975C5.76755 12.8625 6.14505 12.995 6.62005 12.995H7.5V14H6.61255C6.07755 14 5.60755 13.9 5.20255 13.7C4.80255 13.505 4.49255 13.2225 4.27255 12.8525C4.05755 12.4875 3.95255 12.06 3.95755 11.57L3.98755 9.77751Z" fill="#CED0D6"/>
<path d="M12.0145 9.77751C12.0045 9.39251 12.132 9.08501 12.397 8.85501C12.662 8.62501 13.0195 8.51001 13.4695 8.51001H14V7.43001H13.4695C13.0195 7.43001 12.662 7.31501 12.397 7.08501C12.132 6.85501 12.0045 6.54751 12.0145 6.16251L12.0445 4.58C12.0545 4.06 11.9445 3.605 11.7145 3.215C11.4895 2.825 11.1645 2.525 10.7395 2.315C10.3145 2.105 9.81455 2 9.23955 2H8.5V3.005H9.23205C9.75205 3.005 10.162 3.15 10.462 3.44C10.767 3.725 10.917 4.1125 10.912 4.6025L10.882 6.17001C10.872 6.68001 11.0095 7.09751 11.2945 7.42251C11.5795 7.74251 11.9645 7.92001 12.4495 7.95501C11.9645 8.04501 11.5795 8.25501 11.2945 8.58501C11.0095 8.91001 10.872 9.30501 10.882 9.77001L10.912 11.5475C10.922 11.9875 10.787 12.3375 10.507 12.5975C10.232 12.8625 9.85705 12.995 9.38205 12.995H8.5V14H9.38955C9.92455 14 10.392 13.9 10.792 13.7C11.197 13.505 11.507 13.2225 11.722 12.8525C11.942 12.4875 12.0495 12.06 12.0445 11.57L12.0145 9.77751Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB