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