Compare commits

...

12 Commits

Author SHA1 Message Date
hstyi
422e9aac84 release: 1.0.10 2025-03-05 11:11:41 +08:00
hstyi
9915c373b7 chore: remind me next time 2025-02-28 12:43:12 +08:00
hstyi
eba85e6348 fix: emacs shift key 2025-02-27 20:43:51 +08:00
hstyi
483a7772f4 feat: support snippet (#321) 2025-02-27 16:48:25 +08:00
hstyi
dcc96358f6 chore: remind me next time 2025-02-26 16:05:19 +08:00
hstyi
b5c30d505b feat: improve FlatTabbedPaneUI (#314) 2025-02-25 15:45:48 +08:00
hstyi
1f3ef5f3f0 chore: upgrade jdk 21.0.6b895.91 2025-02-25 13:27:41 +08:00
hstyi
d388bcfc92 chore: improve floating toolbar 2025-02-24 18:38:33 +08:00
hstyi
562c1f98fe feat: support to open host by enter 2025-02-24 17:11:12 +08:00
hstyi
f3c5009a45 feat: supports remembering window positions 2025-02-24 16:27:53 +08:00
hstyi
09a1d9f51e chore: osx GitHub actions 2025-02-24 14:31:09 +08:00
hstyi
84b48278ad feat: support sftp status (#307) 2025-02-24 14:14:44 +08:00
62 changed files with 1905 additions and 440 deletions

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
# appimagetool
- run: sudo apt install libfuse2

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
# appimagetool
- run: sudo apt install libfuse2

View File

@@ -33,8 +33,8 @@ jobs:
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary Information
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -44,7 +44,7 @@ jobs:
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
# install jdk
- name: Installing Java
@@ -70,7 +70,7 @@ jobs:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon

View File

@@ -33,8 +33,8 @@ jobs:
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary Information
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -44,7 +44,7 @@ jobs:
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
# install jdk
- name: Installing Java
@@ -72,7 +72,7 @@ jobs:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon

View File

@@ -20,7 +20,7 @@ plugins {
group = "app.termora"
version = "1.0.9"
version = "1.0.10"
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()

View File

@@ -0,0 +1,139 @@
package app.termora;
import com.formdev.flatlaf.ui.FlatTabbedPaneUI;
import com.formdev.flatlaf.ui.FlatUIUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import static com.formdev.flatlaf.FlatClientProperties.*;
import static com.formdev.flatlaf.util.UIScale.scale;
/**
* 如果要升级 FlatLaf 需要检查是否兼容
*/
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
@Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
if (tabPane.getTabCount() <= 0 ||
contentSeparatorHeight == 0 ||
!clientPropertyBoolean(tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, showContentSeparator))
return;
Insets insets = tabPane.getInsets();
Insets tabAreaInsets = getTabAreaInsets(tabPlacement);
int x = insets.left;
int y = insets.top;
int w = tabPane.getWidth() - insets.right - insets.left;
int h = tabPane.getHeight() - insets.top - insets.bottom;
// remove tabs from bounds
switch (tabPlacement) {
case BOTTOM:
h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
h += tabAreaInsets.top;
break;
case LEFT:
x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
x -= tabAreaInsets.right;
w -= (x - insets.left);
break;
case RIGHT:
w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
w += tabAreaInsets.left;
break;
case TOP:
default:
y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
y -= tabAreaInsets.bottom;
h -= (y - insets.top);
break;
}
// compute insets for separator or full border
boolean hasFullBorder = clientPropertyBoolean(tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder);
int sh = scale(contentSeparatorHeight * 100); // multiply by 100 because rotateInsets() does not use floats
Insets ci = new Insets(0, 0, 0, 0);
rotateInsets(hasFullBorder ? new Insets(sh, sh, sh, sh) : new Insets(sh, 0, 0, 0), ci, tabPlacement);
// create path for content separator or full border
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
path.append(new Rectangle2D.Float(x, y, w, h), false);
path.append(new Rectangle2D.Float(x + (ci.left / 100f), y + (ci.top / 100f),
w - (ci.left / 100f) - (ci.right / 100f), h - (ci.top / 100f) - (ci.bottom / 100f)), false);
// add gap for selected tab to path
if (getTabType() == TAB_TYPE_CARD && selectedIndex >= 0) {
float csh = scale((float) contentSeparatorHeight);
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
boolean componentHasFullBorder = false;
if (tabPane.getComponentAt(selectedIndex) instanceof JComponent c) {
componentHasFullBorder = c.getClientProperty(TABBED_PANE_HAS_FULL_BORDER) == Boolean.TRUE;
}
Rectangle2D.Float innerTabRect = new Rectangle2D.Float(tabRect.x + csh, tabRect.y + csh,
componentHasFullBorder ? 0 : tabRect.width - (csh * 2), tabRect.height - (csh * 2));
// Ensure that the separator outside the tabViewport is present (doesn't get cutoff by the active tab)
// If left unsolved the active tab is "visible" in the separator (the gap) even when outside the viewport
if (tabViewport != null)
Rectangle2D.intersect(tabViewport.getBounds(), innerTabRect, innerTabRect);
Rectangle2D.Float gap = null;
if (isHorizontalTabPlacement(tabPlacement)) {
if (innerTabRect.width > 0) {
float y2 = (tabPlacement == TOP) ? y : y + h - csh;
gap = new Rectangle2D.Float(innerTabRect.x, y2, innerTabRect.width, csh);
}
} else {
if (innerTabRect.height > 0) {
float x2 = (tabPlacement == LEFT) ? x : x + w - csh;
gap = new Rectangle2D.Float(x2, innerTabRect.y, csh, innerTabRect.height);
}
}
if (gap != null) {
path.append(gap, false);
// fill gap in case that the tab is colored (e.g. focused or hover)
Color background = getTabBackground(tabPlacement, selectedIndex, true);
g.setColor(FlatUIUtils.deriveColor(background, tabPane.getBackground()));
((Graphics2D) g).fill(gap);
}
}
// paint content separator or full border
g.setColor(contentAreaColor);
((Graphics2D) g).fill(path);
// repaint selection in scroll-tab-layout because it may be painted before
// the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)
if (isScrollTabLayout() && selectedIndex >= 0 && tabViewport != null) {
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
// clip to "scrolling sides" of viewport
// (left and right if horizontal, top and bottom if vertical)
Shape oldClip = g.getClip();
Rectangle vr = tabViewport.getBounds();
if (isHorizontalTabPlacement(tabPlacement))
g.clipRect(vr.x, 0, vr.width, tabPane.getHeight());
else
g.clipRect(0, vr.y, tabPane.getWidth(), vr.height);
paintTabSelection(g, tabPlacement, selectedIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height);
g.setClip(oldClip);
}
}
private boolean isScrollTabLayout() {
return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT;
}
}

View File

@@ -5,6 +5,7 @@ import app.termora.highlight.KeywordHighlight
import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro
import app.termora.snippet.Snippet
import app.termora.sync.SyncType
import app.termora.terminal.CursorStyle
import jetbrains.exodus.bindings.StringBinding
@@ -12,7 +13,6 @@ import jetbrains.exodus.env.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
@@ -26,6 +26,7 @@ class Database private constructor(private val env: Environment) : Disposable {
companion object {
private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host"
private const val SNIPPET_STORE = "Snippet"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair"
@@ -105,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
}
}
fun removeAllHost() {
env.executeInTransaction { tx ->
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
store.openCursor(tx).use {
while (it.next) {
it.deleteCurrent()
}
}
}
}
fun removeAllKeyPair() {
env.executeInTransaction { tx ->
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
@@ -152,15 +142,32 @@ class Database private constructor(private val env: Environment) : Disposable {
}
}
fun removeHost(id: String) {
fun addSnippet(snippet: Snippet) {
var text = ohMyJson.encodeToString(snippet)
if (doorman.isWorking()) {
text = doorman.encrypt(text)
}
env.executeInTransaction {
delete(it, HOST_STORE, id)
put(it, SNIPPET_STORE, snippet.id, text)
if (log.isDebugEnabled) {
log.debug("Removed Host: $id")
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
}
}
}
fun getSnippets(): Collection<Snippet> {
val isWorking = doorman.isWorking()
return env.computeInTransaction { tx ->
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
if (isWorking)
ohMyJson.decodeFromString(doorman.decrypt(value))
else
ohMyJson.decodeFromString(value)
}.values
}
}
fun getKeywordHighlights(): Collection<KeywordHighlight> {
return env.computeInTransaction { tx ->
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
@@ -621,6 +628,7 @@ class Database private constructor(private val env: Environment) : Disposable {
*/
var rangeHosts by BooleanPropertyDelegate(true)
var rangeKeyPairs by BooleanPropertyDelegate(true)
var rangeSnippets by BooleanPropertyDelegate(true)
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
var rangeMacros by BooleanPropertyDelegate(true)
var rangeKeymap by BooleanPropertyDelegate(true)

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.*
import javax.swing.plaf.TabbedPaneUI
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
@@ -21,11 +22,18 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame
private val myUI = MyFlatTabbedPaneUI()
init {
isFocusable = false
super.setUI(myUI)
initEvents()
}
override fun setUI(ui: TabbedPaneUI?) {
super.setUI(myUI)
}
override fun updateUI() {
styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),

View File

@@ -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,46 +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.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
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)
@@ -67,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()
/**
* 是否允许显示右键菜单
@@ -92,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() {
@@ -138,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,145 +140,36 @@ 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
addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
val nodes = getSelectionSimpleTreeNodes()
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
val path = TreePath(model.getPathToRoot(nodes.first()))
if (isExpanded(path)) {
collapsePath(path)
} else {
expandPath(path)
}
} else {
for (node in getSelectionSimpleTreeNodes(true)) {
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e))
}
}
}
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
@@ -426,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),
@@ -453,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)
@@ -462,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)))
}
}
@@ -495,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
@@ -525,28 +320,27 @@ class NewHostTree : JXTree() {
importMenu.isEnabled = lastHost.protocol == Protocol.Folder
// 如果选中了 SSH 服务器,那么才启用
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH }
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
tree.grabFocus()
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
tree.requestFocusInWindow()
}
override fun popupMenuCanceled(e: PopupMenuEvent) {
}
})
popupMenu.show(this, event.x, event.y)
popupMenu.show(this, evt.x, evt.y)
}
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
val lastNode = node as? HostTreeNode ?: return
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
model.nodeStructureChanged(lastNode)
hostManager.addHost(lastNode.host)
}
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
val nNode = node as? HostTreeNode ?: return
val nParent = parent as? HostTreeNode ?: return
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
hostManager.addHost(nNode.host)
}
private fun copyNode(
node: HostTreeNode,
parentId: String,
@@ -583,30 +377,14 @@ class NewHostTree : JXTree() {
}
/**
* 包含孙子
*/
fun getSelectionHostTreeNodes(include: Boolean = false): List<HostTreeNode> {
val paths = selectionPaths ?: return emptyList()
if (paths.isEmpty()) return emptyList()
val nodes = mutableListOf<HostTreeNode>()
val parents = paths.mapNotNull { it.lastPathComponent }
.filterIsInstance<HostTreeNode>().toMutableList()
if (include) {
while (parents.isNotEmpty()) {
val node = parents.removeFirst()
nodes.add(node)
parents.addAll(node.children().toList().filterIsInstance<HostTreeNode>())
}
}
return if (include) nodes else parents
override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
}
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
assertEventDispatchThread()
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
if (nodes.isEmpty()) return
val source = if (openInNewWindow)
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
@@ -615,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
@@ -626,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))
@@ -1089,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)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,343 @@
package app.termora
import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
import java.awt.Component
import java.awt.Dimension
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.*
import javax.swing.*
import javax.swing.event.CellEditorListener
import javax.swing.event.ChangeEvent
import javax.swing.tree.TreePath
import kotlin.math.min
open class SimpleTree : JXTree() {
protected open val model get() = super.getModel() as SimpleTreeModel<*>
private val editor = OutlineTextField(64)
protected val tree get() = this
init {
initViews()
initEvents()
}
private fun initViews() {
// renderer
setCellRenderer(object : DefaultXTreeCellRenderer() {
override fun getTreeCellRendererComponent(
tree: JTree,
value: Any,
sel: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean
): Component {
val node = value as SimpleTreeNode<*>
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
icon = node.getIcon(sel, expanded, hasFocus)
return c
}
})
// rename
setCellEditor(object : DefaultCellEditor(editor) {
override fun isCellEditable(e: EventObject?): Boolean {
if (e is MouseEvent || !tree.isCellEditable(e)) {
return false
}
return super.isCellEditable(e).apply {
if (this) {
editor.preferredSize = Dimension(min(220, width - 64), 0)
}
}
}
override fun getCellEditorValue(): Any? {
return getLastSelectedPathNode()?.data
}
})
}
private fun initEvents() {
// 右键选中
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (!SwingUtilities.isRightMouseButton(e)) {
return
}
requestFocusInWindow()
val selectionRows = selectionModel.selectionRows
val selRow = getClosestRowForLocation(e.x, e.y)
if (selRow < 0) {
selectionModel.clearSelection()
return
} else if (selectionRows != null && selectionRows.contains(selRow)) {
return
}
selectionPath = getPathForLocation(e.x, e.y)
setSelectionRow(selRow)
}
})
// contextmenu
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (!(SwingUtilities.isRightMouseButton(e))) {
return
}
if (Objects.isNull(lastSelectedPathComponent)) {
return
}
showContextmenu(e)
}
})
// rename
getCellEditor().addCellEditorListener(object : CellEditorListener {
override fun editingStopped(e: ChangeEvent) {
val node = getLastSelectedPathNode() ?: return
if (editor.text.isBlank() || editor.text == node.toString()) {
return
}
onRenamed(node, editor.text)
}
override fun editingCanceled(e: ChangeEvent) {
}
})
// drag
transferHandler = object : TransferHandler() {
override fun createTransferable(c: JComponent): Transferable? {
val nodes = getSelectionSimpleTreeNodes().toMutableList()
if (nodes.isEmpty()) return null
if (nodes.contains(model.root)) return null
val iterator = nodes.iterator()
while (iterator.hasNext()) {
val node = iterator.next()
val parents = model.getPathToRoot(node).filter { it != node }
if (parents.any { nodes.contains(it) }) {
iterator.remove()
}
}
return MoveNodeTransferable(nodes)
}
override fun getSourceActions(c: JComponent?): Int {
return MOVE
}
override fun canImport(support: TransferSupport): Boolean {
if (support.component != tree) return false
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
val path = dropLocation.path ?: return false
val node = path.lastPathComponent as? SimpleTreeNode<*> ?: return false
if (!support.isDataFlavorSupported(MoveNodeTransferable.dataFlavor)) return false
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
if (nodes.isEmpty()) return false
if (!node.isFolder) return false
for (e in nodes) {
// 禁止拖拽到自己的子下面
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
return false
}
// 文件夹只能拖拽到文件夹的下面
if (e.isFolder) {
if (dropLocation.childIndex > node.folderCount) {
return false
}
} else if (dropLocation.childIndex != -1) {
// 非文件夹也不能拖拽到文件夹的上面
if (dropLocation.childIndex < node.folderCount) {
return false
}
}
val p = e.parent ?: continue
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
if (p == node && dropLocation.childIndex != -1) {
val idx = p.getIndex(e)
if (dropLocation.childIndex in idx..idx + 1) {
return false
}
}
}
support.setShowDropLocation(true)
return true
}
override fun importData(support: TransferSupport): Boolean {
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
// 展开的 node
val expanded = mutableSetOf(node.id)
for (e in nodes) {
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
.map { it }.forEach { expanded.add(it.id) }
}
// 转移
for (e in nodes) {
model.removeNodeFromParent(e)
rebase(e, node)
if (dropLocation.childIndex == -1) {
if (e.isFolder) {
model.insertNodeInto(e, node, node.folderCount)
} else {
model.insertNodeInto(e, node, node.childCount)
}
} else {
if (e.isFolder) {
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
} else {
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
}
}
selectionPath = TreePath(model.getPathToRoot(e))
}
// 先展开最顶级的
expandPath(TreePath(model.getPathToRoot(node)))
for (child in node.getAllChildren()) {
if (expanded.contains(child.id)) {
expandPath(TreePath(model.getPathToRoot(child)))
}
}
return true
}
}
}
protected open fun newFolder(newNode: SimpleTreeNode<*>): Boolean {
val lastNode = lastSelectedPathComponent
if (lastNode !is SimpleTreeNode<*>) return false
return newNode(newNode, lastNode.folderCount)
}
protected open fun newFile(newNode: SimpleTreeNode<*>): Boolean {
val lastNode = lastSelectedPathComponent
if (lastNode !is SimpleTreeNode<*>) return false
return newNode(newNode, lastNode.childCount)
}
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
val lastNode = lastSelectedPathComponent
if (lastNode !is SimpleTreeNode<*>) return false
model.insertNodeInto(newNode, lastNode, index)
selectionPath = TreePath(model.getPathToRoot(newNode))
startEditingAtPath(selectionPath)
return true
}
open fun getLastSelectedPathNode(): SimpleTreeNode<*>? {
return lastSelectedPathComponent as? SimpleTreeNode<*>
}
protected open fun showContextmenu(evt: MouseEvent) {
}
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
protected open fun refreshNode(node: SimpleTreeNode<*>) {
val state = TreeUtils.saveExpansionState(tree)
val rows = selectionRows
model.reload(node)
TreeUtils.loadExpansionState(tree, state)
super.setSelectionRows(rows)
}
/**
* 包含孙子
*/
open fun getSelectionSimpleTreeNodes(include: Boolean = false): List<SimpleTreeNode<*>> {
val paths = selectionPaths ?: return emptyList()
if (paths.isEmpty()) return emptyList()
val nodes = mutableListOf<SimpleTreeNode<*>>()
val parents = paths.mapNotNull { it.lastPathComponent }
.filterIsInstance<SimpleTreeNode<*>>().toMutableList()
if (include) {
while (parents.isNotEmpty()) {
val node = parents.removeFirst()
nodes.add(node)
parents.addAll(node.children().toList().filterIsInstance<SimpleTreeNode<*>>())
}
}
return if (include) nodes else parents
}
protected open fun isCellEditable(e: EventObject?): Boolean {
return getLastSelectedPathNode() != model.root
}
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
}
private class MoveNodeTransferable(val nodes: List<SimpleTreeNode<*>>) : Transferable {
companion object {
val dataFlavor =
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveNodeTransferable::class.java.name}")
}
override fun getTransferDataFlavors(): Array<DataFlavor> {
return arrayOf(dataFlavor)
}
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
return dataFlavor == flavor
}
override fun getTransferData(flavor: DataFlavor?): Any {
if (flavor == dataFlavor) {
return nodes
}
throw UnsupportedFlavorException(flavor)
}
}
}

View File

@@ -0,0 +1,11 @@
package app.termora
import javax.swing.tree.DefaultTreeModel
abstract class SimpleTreeModel<T>(root: SimpleTreeNode<T>) : DefaultTreeModel(root) {
@Suppress("UNCHECKED_CAST")
override fun getRoot(): SimpleTreeNode<T> {
return super.getRoot() as SimpleTreeNode<T>
}
}

View File

@@ -0,0 +1,37 @@
package app.termora
import javax.swing.Icon
import javax.swing.tree.DefaultMutableTreeNode
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
@Suppress("UNCHECKED_CAST")
open var data: T
get() = userObject as T
set(value) = setUserObject(value)
@Suppress("UNCHECKED_CAST")
override fun getParent(): SimpleTreeNode<T>? {
return super.getParent() as SimpleTreeNode<T>?
}
open val folderCount: Int get() = 0
open fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon? {
return null
}
open val isFolder get() = false
abstract val id: String
@Suppress("UNCHECKED_CAST")
open fun getAllChildren(): List<SimpleTreeNode<T>> {
val children = mutableListOf<SimpleTreeNode<T>>()
for (child in children()) {
val c = child as? SimpleTreeNode<T> ?: continue
children.add(c)
children.addAll(c.getAllChildren())
}
return children
}
}

View File

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

View File

@@ -78,21 +78,14 @@ class TerminalTabbed(
tabs[oldIndex].onLostFocus()
}
tabbedPane.getComponentAt(newIndex).requestFocusInWindow()
if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus()
}
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
}
// 选择变动
tabbedPane.addChangeListener {
if (tabbedPane.selectedIndex >= 0) {
val c = tabbedPane.getComponentAt(tabbedPane.selectedIndex)
c.requestFocusInWindow()
}
}
// 右键菜单
tabbedPane.addMouseListener(object : MouseAdapter() {

View File

@@ -2,10 +2,13 @@ package app.termora
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory
import java.awt.Frame
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.JOptionPane
import javax.swing.UIManager
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
import kotlin.math.max
import kotlin.system.exitProcess
class TermoraFrameManager {
@@ -20,16 +23,32 @@ class TermoraFrameManager {
}
private val frames = mutableListOf<TermoraFrame>()
private val properties get() = Database.getDatabase().properties
fun createWindow(): TermoraFrame {
val frame = TermoraFrame()
registerCloseCallback(frame)
val frame = TermoraFrame().apply { registerCloseCallback(this) }
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frames.add(frame)
return frame
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
if (rectangle.isMaximized) {
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frame.extendedState = rectangle.s
} else {
// 控制最小
frame.setSize(
max(rectangle.w, UIManager.getInt("Dialog.width") - 150),
max(rectangle.h, UIManager.getInt("Dialog.height") - 100)
)
if (rectangle.x == -1 && rectangle.y == -1) {
frame.setLocationRelativeTo(null)
} else {
frame.setLocation(max(rectangle.x, 0), max(rectangle.y, 0))
}
}
return frame.apply { frames.add(this) }
}
fun getWindows(): Array<TermoraFrame> {
@@ -41,6 +60,9 @@ class TermoraFrameManager {
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
// 存储位置信息
saveFrameRectangle(window)
// 删除
frames.remove(window)
@@ -87,4 +109,27 @@ class TermoraFrameManager {
exitProcess(0)
}
private fun saveFrameRectangle(frame: TermoraFrame) {
properties.putString("TermoraFrame.x", frame.x.toString())
properties.putString("TermoraFrame.y", frame.y.toString())
properties.putString("TermoraFrame.width", frame.width.toString())
properties.putString("TermoraFrame.height", frame.height.toString())
properties.putString("TermoraFrame.extendedState", frame.extendedState.toString())
}
private fun getFrameRectangle(): FrameRectangle? {
val x = properties.getString("TermoraFrame.x")?.toIntOrNull() ?: return null
val y = properties.getString("TermoraFrame.y")?.toIntOrNull() ?: return null
val w = properties.getString("TermoraFrame.width")?.toIntOrNull() ?: return null
val h = properties.getString("TermoraFrame.height")?.toIntOrNull() ?: return null
val s = properties.getString("TermoraFrame.extendedState")?.toIntOrNull() ?: return null
return FrameRectangle(x, y, w, h, s)
}
private data class FrameRectangle(
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
) {
val isMaximized get() = (s and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH
}
}

View File

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

View File

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

View File

@@ -60,7 +60,6 @@ class UpdaterManager private constructor() {
val isSelf get() = this == self
}
private val properties get() = Database.getDatabase().properties
var lastVersion = LatestVersion.self
fun fetchLatestVersion(): LatestVersion {
@@ -146,12 +145,4 @@ class UpdaterManager private constructor() {
return LatestVersion.self
}
fun isIgnored(version: String): Boolean {
return properties.getString("ignored.version.$version", "false").toBoolean()
}
fun ignore(version: String) {
properties.putString("ignored.version.$version", "true")
}
}

View File

@@ -13,7 +13,9 @@ import com.formdev.flatlaf.extras.components.FlatTextField
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.awt.event.ActionEvent
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
@@ -32,6 +34,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
private val dataProviderSupport = DataProviderSupport()
private val hostTreeModel = hostTree.model as NewHostTreeModel
private var lastFocused: Component? = null
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
searchTextField.text.isBlank()
}
@@ -258,6 +261,14 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
return false
}
override fun onLostFocus() {
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
}
override fun onGrabFocus() {
SwingUtilities.invokeLater { lastFocused?.requestFocusInWindow() }
}
override fun dispose() {
properties.putString("WelcomeFullContent", fullContent.toString())
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))

View File

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

View File

@@ -47,6 +47,7 @@ class AppUpdateAction private constructor() : AnAction(
}
private val updaterManager get() = UpdaterManager.getInstance()
private var isRemindMeNextTime = false
init {
isEnabled = false
@@ -65,7 +66,9 @@ class AppUpdateAction private constructor() : AnAction(
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
if (!isRemindMeNextTime) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
}
@@ -82,10 +85,6 @@ class AppUpdateAction private constructor() : AnAction(
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
try {
downloadLatestPkg(latestVersion)
} catch (e: Exception) {
@@ -194,7 +193,7 @@ class AppUpdateAction private constructor() : AnAction(
return
} else if (option == JOptionPane.NO_OPTION) {
isEnabled = false
updaterManager.ignore(lastVersion.version)
isRemindMeNextTime = true
} else if (option == JOptionPane.YES_OPTION) {
updateSelf(lastVersion)
}

View File

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

View File

@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
object DataProviders {
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)

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
package app.termora.snippet
import app.termora.toSimpleString
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils
import java.util.*
enum class SnippetType {
Folder,
Snippet,
}
@Serializable
data class Snippet(
val id: String = UUID.randomUUID().toSimpleString(),
val name: String,
val snippet: String = StringUtils.EMPTY,
val parentId: String = StringUtils.EMPTY,
val type: SnippetType = SnippetType.Snippet,
val deleted: Boolean = false,
val sort: Long = System.currentTimeMillis(),
val createDate: Long = System.currentTimeMillis(),
val updateDate: Long = System.currentTimeMillis(),
)

View File

@@ -0,0 +1,47 @@
package app.termora.snippet
import app.termora.ApplicationScope
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.Terminal
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
companion object {
fun getInstance(): SnippetAction {
return ApplicationScope.forApplicationScope().getOrCreate(SnippetAction::class) { SnippetAction() }
}
const val SNIPPET = "SnippetAction"
}
override fun actionPerformed(evt: AnActionEvent) {
SnippetDialog(evt.window).isVisible = true
}
fun runSnippet(snippet: Snippet, terminal: Terminal) {
if (snippet.type != SnippetType.Snippet) return
val terminalModel = terminal.getTerminalModel()
val map = mapOf(
"\\r" to ControlCharacters.CR,
"\\n" to ControlCharacters.LF,
"\\t" to ControlCharacters.TAB,
"\\a" to ControlCharacters.BEL,
"\\e" to ControlCharacters.ESC,
"\\b" to ControlCharacters.BS,
)
if (terminalModel.hasData(DataKey.PtyConnector)) {
var text = snippet.snippet
for (e in map.entries) {
text = text.replace(e.key, e.value.toString())
}
val ptyConnector = terminalModel.getData(DataKey.PtyConnector)
ptyConnector.write(text.toByteArray(ptyConnector.getCharset()))
}
}
}

View File

@@ -0,0 +1,50 @@
package app.termora.snippet
import java.awt.*
import javax.swing.JComponent
import javax.swing.UIManager
class SnippetBannerPanel(fontSize: Int = 12) : JComponent() {
private val banner = """
_____ _ __
/ ___/____ (_)___ ____ ___ / /_
\__ \/ __ \/ / __ \/ __ \/ _ \/ __/
___/ / / / / / /_/ / /_/ / __/ /_
/____/_/ /_/_/ .___/ .___/\___/\__/
/_/ /_/
""".trimIndent().lines()
init {
font = Font("JetBrains Mono", Font.PLAIN, fontSize)
preferredSize = Dimension(width, getFontMetrics(font).height * banner.size)
size = preferredSize
}
override fun paintComponent(g: Graphics) {
if (g is Graphics2D) {
g.setRenderingHints(
RenderingHints(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
)
)
}
g.font = font
g.color = UIManager.getColor("TextField.placeholderForeground")
val height = g.fontMetrics.height
val descent = g.fontMetrics.descent
val offset = width / 2 - g.fontMetrics.stringWidth(banner.maxBy { it.length }) / 2
for (i in banner.indices) {
var x = offset
val y = height * (i + 1) - descent
val chars = banner[i].toCharArray()
for (j in chars.indices) {
g.drawChars(chars, j, 1, x, y)
x += g.fontMetrics.charWidth(chars[j])
}
}
}
}

View File

@@ -0,0 +1,59 @@
package app.termora.snippet
import app.termora.*
import java.awt.Dimension
import java.awt.Window
import javax.swing.JComponent
import javax.swing.UIManager
import kotlin.math.max
class SnippetDialog(owner: Window) : DialogWrapper(owner) {
private val properties get() = Database.getDatabase().properties
init {
initViews()
initEvents()
init()
}
private fun initViews() {
val w = properties.getString("SnippetDialog.width", "0").toIntOrNull() ?: 0
val h = properties.getString("SnippetDialog.height", "0").toIntOrNull() ?: 0
val x = properties.getString("SnippetDialog.x", "-1").toIntOrNull() ?: -1
val y = properties.getString("SnippetDialog.y", "-1").toIntOrNull() ?: -1
size = if (w > 0 && h > 0) {
Dimension(w, h)
} else {
Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
}
isModal = true
isResizable = true
title = I18n.getString("termora.snippet.title")
if (x != -1 && y != -1) {
setLocation(max(x, 0), max(y, 0))
} else {
setLocationRelativeTo(owner)
}
}
private fun initEvents() {
Disposer.register(disposable, object : Disposable {
override fun dispose() {
properties.putString("SnippetDialog.width", width.toString())
properties.putString("SnippetDialog.height", height.toString())
properties.putString("SnippetDialog.x", x.toString())
properties.putString("SnippetDialog.y", y.toString())
}
})
}
override fun createCenterPanel(): JComponent {
return SnippetPanel().apply { Disposer.register(disposable, this) }
}
override fun createSouthPanel(): JComponent? {
return null
}
}

View File

@@ -0,0 +1,44 @@
package app.termora.snippet
import app.termora.ApplicationScope
import app.termora.Database
import app.termora.assertEventDispatchThread
class SnippetManager private constructor() {
companion object {
fun getInstance(): SnippetManager {
return ApplicationScope.forApplicationScope().getOrCreate(SnippetManager::class) { SnippetManager() }
}
}
private val database get() = Database.getDatabase()
private var snippets = mutableMapOf<String, Snippet>()
/**
* 修改缓存并存入数据库
*/
fun addSnippet(snippet: Snippet) {
assertEventDispatchThread()
database.addSnippet(snippet)
if (snippet.deleted) {
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id }
} else {
snippets[snippet.id] = snippet
}
}
/**
* 第一次调用从数据库中获取,后续从缓存中获取
*/
fun snippets(): List<Snippet> {
if (snippets.isEmpty()) {
database.getSnippets().filter { !it.deleted }
.forEach { snippets[it.id] = it }
}
return snippets.values.filter { !it.deleted }
.sortedWith(compareBy<Snippet> { if (it.type == SnippetType.Folder) 0 else 1 }.thenBy { it.sort })
}
}

View File

@@ -0,0 +1,225 @@
package app.termora.snippet
import app.termora.*
import com.formdev.flatlaf.extras.components.FlatTextArea
import com.formdev.flatlaf.ui.FlatRoundBorder
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.CardLayout
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import javax.swing.*
import javax.swing.event.DocumentEvent
import javax.swing.undo.UndoManager
class SnippetPanel : JPanel(BorderLayout()), Disposable {
companion object {
private val log = LoggerFactory.getLogger(SnippetPanel::class.java)
private val properties get() = Database.getDatabase().properties
private val snippetManager get() = SnippetManager.getInstance()
}
private val leftPanel = JPanel(BorderLayout())
private val cardLayout = CardLayout()
private val rightPanel = JPanel(cardLayout)
private val snippetTree = SnippetTree()
private val editor = SnippetEditor()
private val lastNode get() = snippetTree.getLastSelectedPathNode()
init {
initViews()
initEvents()
}
private fun initViews() {
val splitPane = JSplitPane()
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
leftPanel.add(snippetTree, BorderLayout.CENTER)
leftPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(4, 4, 4, 4)
)
leftPanel.preferredSize = Dimension(
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
-1
)
rightPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(6, 6, 6, 6)
)
val bannerPanel = JPanel(BorderLayout())
bannerPanel.add(SnippetBannerPanel(), BorderLayout.CENTER)
bannerPanel.border = BorderFactory.createEmptyBorder(32, 0, 0, 0)
rightPanel.add(bannerPanel, "Banner")
rightPanel.add(editor, "Editor")
splitPane.leftComponent = leftPanel
splitPane.rightComponent = rightPanel
add(splitPane, BorderLayout.CENTER)
cardLayout.show(rightPanel, "Banner")
}
private fun initEvents() {
snippetTree.addTreeSelectionListener {
val lastNode = this.lastNode
if (lastNode == null || lastNode.isFolder) {
cardLayout.show(rightPanel, "Banner")
} else {
cardLayout.show(rightPanel, "Editor")
editor.textArea.text = lastNode.data.snippet
editor.resetUndo()
}
}
SwingUtilities.invokeLater {
if (snippetTree.selectionRows?.isEmpty() == true) {
snippetTree.addSelectionRow(0)
}
snippetTree.requestFocusInWindow()
}
val expansionState = properties.getString("SnippetPanel.LeftTreePanel.expansionState", StringUtils.EMPTY)
if (expansionState.isNotBlank()) {
TreeUtils.loadExpansionState(snippetTree, expansionState)
}
val selectionRows = properties.getString("SnippetPanel.LeftTreePanel.selectionRows", StringUtils.EMPTY)
if (selectionRows.isNotBlank()) {
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
}
}
override fun dispose() {
properties.putString("SnippetPanel.LeftPanel.width", leftPanel.width.toString())
properties.putString("SnippetPanel.LeftPanel.height", leftPanel.height.toString())
properties.putString("SnippetPanel.LeftTreePanel.expansionState", TreeUtils.saveExpansionState(snippetTree))
properties.putString("SnippetPanel.LeftTreePanel.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
}
private inner class SnippetEditor : JPanel(BorderLayout()) {
val textArea = FlatTextArea()
private var undoManager = UndoManager()
init {
initViews()
initEvents()
}
private fun initViews() {
val panel = JPanel(BorderLayout())
panel.add(JScrollPane(textArea).apply { border = BorderFactory.createEmptyBorder() }, BorderLayout.CENTER)
panel.border = FlatRoundBorder()
add(panel, BorderLayout.CENTER)
add(createTip(), BorderLayout.SOUTH)
textArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
textArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
}
private fun initEvents() {
textArea.document.addUndoableEditListener(undoManager)
textArea.addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if ((e.keyCode == KeyEvent.VK_Z || e.keyCode == KeyEvent.VK_Y) && (if (SystemInfo.isMacOS) e.isMetaDown else e.isControlDown)) {
try {
if (e.keyCode == KeyEvent.VK_Z) {
if (undoManager.canUndo()) {
undoManager.undo()
}
} else {
if (undoManager.canRedo()) {
undoManager.redo()
}
}
} catch (cue: Exception) {
if (log.isErrorEnabled) {
log.error(cue.message, cue.message)
}
}
}
}
})
textArea.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
val lastNode = lastNode ?: return
lastNode.data = lastNode.data.copy(snippet = textArea.text, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(lastNode.data)
}
})
}
fun resetUndo() {
textArea.document.removeUndoableEditListener(undoManager)
undoManager = UndoManager()
textArea.document.addUndoableEditListener(undoManager)
}
private fun createTip(): JPanel {
val formMargin = "10dlu"
val panel = FormBuilder.create().debug(false)
.border(
BorderFactory.createCompoundBorder(
FlatRoundBorder(),
BorderFactory.createEmptyBorder(2, 4, 4, 4)
)
)
.layout(
FormLayout(
"left:pref, left:pref, $formMargin, left:pref, left:pref, $formMargin, left:pref, left:pref",
"pref, $formMargin, pref"
)
)
.add(createTipLabel("\\r - ")).xy(1, 1)
.add(createTipLabel("CR")).xy(2, 1)
.add(createTipLabel("\\n - ")).xy(4, 1)
.add(createTipLabel("LF")).xy(5, 1)
.add(createTipLabel("\\t - ")).xy(7, 1)
.add(createTipLabel("Tab")).xy(8, 1)
.add(createTipLabel("\\a - ")).xy(1, 2)
.add(createTipLabel("Bell")).xy(2, 2)
.add(createTipLabel("\\e - ")).xy(4, 2)
.add(createTipLabel("Escape")).xy(5, 2)
.add(createTipLabel("\\b - ")).xy(7, 2)
.add(createTipLabel("Backspace")).xy(8, 2)
.build()
return JPanel(BorderLayout()).apply {
add(panel, BorderLayout.CENTER)
border = BorderFactory.createEmptyBorder(4, 0, 0, 0)
}
}
private fun createTipLabel(text: String): JLabel {
val label = JLabel(text)
label.foreground = UIManager.getColor("textInactiveText")
return label
}
}
}

View File

@@ -0,0 +1,146 @@
package app.termora.snippet
import app.termora.I18n
import app.termora.OptionPane
import app.termora.SimpleTree
import app.termora.SimpleTreeNode
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import java.awt.event.MouseEvent
import javax.swing.DropMode
import javax.swing.JMenu
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
import javax.swing.tree.TreePath
class SnippetTree : SimpleTree() {
override val model = SnippetTreeModel()
private val snippetManager get() = SnippetManager.getInstance()
init {
initViews()
initEvents()
}
private fun initViews() {
super.setModel(model)
isEditable = true
dragEnabled = true
dropMode = DropMode.ON_OR_INSERT
}
private fun initEvents() {
}
override fun showContextmenu(evt: MouseEvent) {
val lastNode = getLastSelectedPathNode() ?: return
val popupMenu = FlatPopupMenu()
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
val newSnippet = newMenu.add(I18n.getString("termora.snippet"))
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
popupMenu.addSeparator()
val refresh = popupMenu.add(I18n.getString("termora.welcome.contextmenu.refresh"))
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
popupMenu.addSeparator()
newFolder.addActionListener {
val snippet = Snippet(
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
type = SnippetType.Folder,
parentId = lastNode.data.id
)
snippetManager.addSnippet(snippet)
newFolder(SnippetTreeNode(snippet))
}
newSnippet.addActionListener {
val snippet = Snippet(
name = I18n.getString("termora.snippet"),
type = SnippetType.Snippet,
parentId = lastNode.data.id
)
snippetManager.addSnippet(snippet)
newFile(SnippetTreeNode(snippet))
}
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
refresh.addActionListener { refreshNode(lastNode) }
expandAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true)) {
expandPath(TreePath(model.getPathToRoot(node)))
}
}
colspanAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
collapsePath(TreePath(model.getPathToRoot(node)))
}
}
remove.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val nodes = getSelectionSimpleTreeNodes()
if (nodes.isEmpty()) return
if (OptionPane.showConfirmDialog(
SwingUtilities.getWindowAncestor(tree),
I18n.getString("termora.keymgr.delete-warning"),
I18n.getString("termora.remove"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
) == JOptionPane.YES_OPTION
) {
for (c in nodes) {
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
model.removeNodeFromParent(c)
// 将所有子孙也删除
for (child in c.getAllChildren()) {
snippetManager.addSnippet(
child.data.copy(
deleted = true,
updateDate = System.currentTimeMillis()
)
)
}
}
}
}
})
rename.isEnabled = lastNode != model.root
remove.isEnabled = rename.isEnabled
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
newSnippet.isEnabled = newFolder.isEnabled
newMenu.isEnabled = newFolder.isEnabled
refresh.isEnabled = newFolder.isEnabled
popupMenu.add(newMenu)
popupMenu.show(this, evt.x, evt.y)
}
public override fun getLastSelectedPathNode(): SnippetTreeNode? {
return super.getLastSelectedPathNode() as? SnippetTreeNode
}
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
val n = node as? SnippetTreeNode ?: return
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(n.data)
model.nodeStructureChanged(n)
}
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
val nNode = node as? SnippetTreeNode ?: return
val nParent = parent as? SnippetTreeNode ?: return
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(nNode.data)
}
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<SnippetTreeNode>()
}
}

View File

@@ -0,0 +1,65 @@
package app.termora.snippet
import app.termora.*
import org.apache.commons.lang3.StringUtils
import java.awt.Dimension
import java.awt.Window
import javax.swing.BorderFactory
import javax.swing.JComponent
import javax.swing.JScrollPane
class SnippetTreeDialog(owner: Window) : DialogWrapper(owner) {
private val snippetTree = SnippetTree()
private val properties get() = Database.getDatabase().properties
var lastNode: SnippetTreeNode? = null
init {
size = Dimension(360, 380)
title = I18n.getString("termora.snippet.title")
isModal = true
isResizable = true
controlsVisible = false
setLocationRelativeTo(null)
init()
Disposer.register(disposable, object : Disposable {
override fun dispose() {
properties.putString("SnippetTreeDialog.Tree.expansionState", TreeUtils.saveExpansionState(snippetTree))
properties.putString("SnippetTreeDialog.Tree.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
}
})
val expansionState = properties.getString("SnippetTreeDialog.Tree.expansionState", StringUtils.EMPTY)
if (expansionState.isNotBlank()) {
TreeUtils.loadExpansionState(snippetTree, expansionState)
}
val selectionRows = properties.getString("SnippetTreeDialog.Tree.selectionRows", StringUtils.EMPTY)
if (selectionRows.isNotBlank()) {
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
}
}
override fun createCenterPanel(): JComponent {
return JScrollPane(snippetTree).apply { border = BorderFactory.createEmptyBorder(0, 6, 6, 6) }
}
override fun doCancelAction() {
lastNode = null
super.doCancelAction()
}
override fun doOKAction() {
val node = snippetTree.getLastSelectedPathNode() ?: return
if (node.isFolder) return
lastNode = node
super.doOKAction()
}
fun getSelectedNode(): SnippetTreeNode? {
return lastNode
}
}

View File

@@ -0,0 +1,73 @@
package app.termora.snippet
import app.termora.SimpleTreeModel
import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode
class SnippetTreeModel : SimpleTreeModel<Snippet>(
SnippetTreeNode(
Snippet(
id = "0",
name = "全部片段",
type = SnippetType.Folder
)
)
) {
private val snippetManager get() = SnippetManager.getInstance()
init {
reload()
}
override fun getRoot(): SnippetTreeNode {
return super.getRoot() as SnippetTreeNode
}
override fun reload(parent: TreeNode?) {
if (parent !is SnippetTreeNode) {
super.reload(parent)
return
}
parent.removeAllChildren()
val hosts = snippetManager.snippets()
val nodes = linkedMapOf<String, SnippetTreeNode>()
// 遍历 Host 列表,构建树节点
for (host in hosts) {
val node = SnippetTreeNode(host)
nodes[host.id] = node
}
for (host in hosts) {
val node = nodes[host.id] ?: continue
if (host.parentId.isBlank()) continue
val p = nodes[host.parentId] ?: continue
p.add(node)
}
for ((_, v) in nodes.entries) {
if (parent.data.id == v.data.parentId) {
parent.add(v)
}
}
super.reload(parent)
}
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
super.insertNodeInto(newChild, parent, index)
// 重置所有排序
if (parent is SnippetTreeNode) {
for ((i, c) in parent.children().toList().filterIsInstance<SnippetTreeNode>().withIndex()) {
val sort = i.toLong()
if (c.data.sort == sort) continue
c.data = c.data.copy(sort = sort, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(c.data)
}
}
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.snippet
import app.termora.Icons
import app.termora.SimpleTreeNode
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
import javax.swing.Icon
class SnippetTreeNode(snippet: Snippet) : SimpleTreeNode<Snippet>(snippet) {
override val folderCount: Int
get() = children().toList().count { if (it is SnippetTreeNode) it.data.type == SnippetType.Folder else false }
override val id get() = data.id
override val isFolder get() = data.type == SnippetType.Folder
override fun toString(): String {
return data.name
}
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
return when (data.type) {
SnippetType.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
else -> if (selected && hasFocus) Icons.codeSpan.dark else Icons.codeSpan
}
}
}

View File

@@ -62,6 +62,14 @@ abstract class GitSyncer : SafetySyncer() {
}
}
// decode Snippets
if (config.ranges.contains(SyncRange.Snippets)) {
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
decodeSnippets(it.content, config)
}
}
if (log.isInfoEnabled) {
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)

View File

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

View File

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

View File

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

View File

@@ -384,7 +384,7 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
val mode = args.toInt(0)
if (mode == 0) {
val x = terminal.getCursorModel().getPosition().x
terminal.getTabulator().clearTabStop(x)
terminal.getTabulator().clearTabStop(x - 1)
if (log.isDebugEnabled) {
log.debug("Tab Clear (TBC). clearTabStop($x)")
}

View File

@@ -171,7 +171,7 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
}
} else {
val x = terminal.getCursorModel().getPosition().x
terminal.getTabulator().setTabStop(x)
terminal.getTabulator().setTabStop(x - 1)
if (log.isDebugEnabled) {
log.debug("Horizontal Tab Set (HTS). col: $x")
}

View File

@@ -55,14 +55,14 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F2), encode = "${ControlCharacters.ESC}OQ")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F3), encode = "${ControlCharacters.ESC}OR")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F4), encode = "${ControlCharacters.ESC}OS")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F5), encode = "${ControlCharacters.ESC}[15~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F6), encode = "${ControlCharacters.ESC}[17~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F7), encode = "${ControlCharacters.ESC}[18~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F8), encode = "${ControlCharacters.ESC}[19~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F9), encode = "${ControlCharacters.ESC}[20~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F10), encode = "${ControlCharacters.ESC}[21~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F11), encode = "${ControlCharacters.ESC}[23~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F12), encode = "${ControlCharacters.ESC}[24~");
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F5), encode = "${ControlCharacters.ESC}[15~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F6), encode = "${ControlCharacters.ESC}[17~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F7), encode = "${ControlCharacters.ESC}[18~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F8), encode = "${ControlCharacters.ESC}[19~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F9), encode = "${ControlCharacters.ESC}[20~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F10), encode = "${ControlCharacters.ESC}[21~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F11), encode = "${ControlCharacters.ESC}[23~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F12), encode = "${ControlCharacters.ESC}[24~")
terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) {
@@ -73,7 +73,40 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
}
override fun encode(event: TerminalKeyEvent): String {
return mapping[event] ?: nothing
if (mapping.containsKey(event)) {
return mapping.getValue(event)
}
var bytes = (mapping[TerminalKeyEvent(event.keyCode, 0)] ?: return nothing).toByteArray()
if (alwaysSendEsc(event.keyCode) && (event.modifiers and TerminalEvent.ALT_MASK) != 0) {
bytes = insertCodeAt(bytes, makeCode(ControlCharacters.ESC.code), 0)
return String(bytes)
}
if (alwaysSendEsc(event.keyCode) && (event.modifiers and TerminalEvent.META_MASK) != 0) {
bytes = insertCodeAt(bytes, makeCode(ControlCharacters.ESC.code), 0)
return String(bytes)
}
if (isCursorKey(event.keyCode) || isFunctionKey(event.keyCode)) {
bytes = getCodeWithModifiers(bytes, event.modifiers)
return String(bytes)
}
return String(bytes)
}
private fun makeCode(vararg bytesAsInt: Int): ByteArray {
val bytes = ByteArray(bytesAsInt.size)
for ((i, byteAsInt) in bytesAsInt.withIndex()) {
bytes[i] = byteAsInt.toByte()
}
return bytes
}
private fun alwaysSendEsc(key: Int): Boolean {
return isCursorKey(key) || key == '\b'.code
}
override fun getTerminal(): Terminal {
@@ -84,6 +117,91 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
mapping[event] = encode
}
/**
* Refer to section PC-Style Function Keys in http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
*/
private fun getCodeWithModifiers(bytes: ByteArray, modifiers: Int): ByteArray {
val code = modifiersToCode(modifiers)
if (code > 0 && bytes.size > 2) {
// SS3 needs to become CSI.
if (bytes[0].toInt() == ControlCharacters.ESC.code && bytes[1] == 'O'.code.toByte()) {
bytes[1] = '['.code.toByte()
}
// If the control sequence has no parameters, it needs a default parameter.
// Either way it also needs a semicolon separator.
val prefix = if (bytes.size == 3) "1;" else ";"
return insertCodeAt(
bytes,
(prefix + code).toByteArray(),
bytes.size - 1
)
}
return bytes
}
private fun insertCodeAt(bytes: ByteArray, code: ByteArray, at: Int): ByteArray {
val res = ByteArray(bytes.size + code.size)
System.arraycopy(bytes, 0, res, 0, bytes.size)
System.arraycopy(bytes, at, res, at + code.size, bytes.size - at)
System.arraycopy(code, 0, res, at, code.size)
return res
}
/**
*
* Code Modifiers
* ------+--------------------------
* 2 | Shift
* 3 | Alt
* 4 | Shift + Alt
* 5 | Control
* 6 | Shift + Control
* 7 | Alt + Control
* 8 | Shift + Alt + Control
* 9 | Meta
* 10 | Meta + Shift
* 11 | Meta + Alt
* 12 | Meta + Alt + Shift
* 13 | Meta + Ctrl
* 14 | Meta + Ctrl + Shift
* 15 | Meta + Ctrl + Alt
* 16 | Meta + Ctrl + Alt + Shift
* ------+--------------------------
* @param modifiers
* @return
*/
private fun modifiersToCode(modifiers: Int): Int {
var code = 0
if ((modifiers and TerminalEvent.SHIFT_MASK) != 0) {
code = code or 1
}
if ((modifiers and TerminalEvent.ALT_MASK) != 0) {
code = code or 2
}
if ((modifiers and TerminalEvent.CTRL_MASK) != 0) {
code = code or 4
}
if ((modifiers and TerminalEvent.META_MASK) != 0) {
code = code or 8
}
return if (code != 0) code + 1 else 0
}
private fun isCursorKey(key: Int): Boolean {
return key == KeyEvent.VK_DOWN || key == KeyEvent.VK_UP
|| key == KeyEvent.VK_LEFT || key == KeyEvent.VK_RIGHT
|| key == KeyEvent.VK_HOME || key == KeyEvent.VK_END
}
private fun isFunctionKey(key: Int): Boolean {
return key >= KeyEvent.VK_F1 && key <= KeyEvent.VK_F12
|| key == KeyEvent.VK_INSERT || key == KeyEvent.VK_DELETE
|| key == KeyEvent.VK_PAGE_UP || key == KeyEvent.VK_PAGE_DOWN
}
fun arrowKeysApplicationSequences() {
// Up
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")

View File

@@ -1,7 +1,7 @@
package app.termora.terminal
import org.slf4j.LoggerFactory
import kotlin.math.min
import kotlin.math.max
open class VisualTerminal : Terminal {
@@ -119,6 +119,9 @@ open class VisualTerminal : Terminal {
private class MyProcessor(private val terminal: Terminal, reader: TerminalReader) {
private var state: ProcessorState = TerminalState.READY
private val document get() = terminal.getDocument()
private val cursorModel get() = terminal.getCursorModel()
private val terminalModel get() = terminal.getTerminalModel()
companion object {
private val log = LoggerFactory.getLogger(MyProcessor::class.java)
@@ -135,7 +138,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
fun process(ch: Char) {
if (log.isTraceEnabled) {
val position = terminal.getCursorModel().getPosition()
val position = cursorModel.getPosition()
log.trace("process [${printChar(ch)}] , state: $state , position: $position")
}
@@ -155,16 +158,29 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
}
ControlCharacters.CR -> {
terminal.getCursorModel().move(CursorMove.RowHome)
cursorModel.move(CursorMove.RowHome)
TerminalState.READY
}
ControlCharacters.TAB -> {
val position = terminal.getCursorModel().getPosition()
val position = cursorModel.getPosition()
// Next tab + 1如果当前 x = 11那么下一个就是 16因为在 TerminalLineBuffer#writeTerminalLineChar 的时候会 - 1 会导致错乱一位
var nextTab = terminal.getTabulator().nextTab(position.x) + 1
nextTab = min(terminal.getTerminalModel().getCols(), nextTab)
terminal.getCursorModel().move(row = position.y, col = nextTab)
val nextTab = terminal.getTabulator().nextTab(position.x - 1) + 1
val length = if (terminalModel.isAlternateScreenBuffer()) {
document.getCurrentTerminalLineBuffer()
.getLineAt(position.y - 1).getText().length
} else {
document.getCurrentTerminalLineBuffer()
.getScreenLineAt(position.y - 1)
.getText().length
}
val x = max(position.x - 1, length)
if (x < nextTab) {
cursorModel.move(row = position.y, col = (position.x - 1) + (nextTab - x))
} else {
cursorModel.move(row = position.y, col = nextTab)
}
TerminalState.READY
}
@@ -176,12 +192,12 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
}
ControlCharacters.BS -> {
terminal.getCursorModel().move(CursorMove.Left)
cursorModel.move(CursorMove.Left)
TerminalState.READY
}
ControlCharacters.SI -> {
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G0)
terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G0)
if (log.isDebugEnabled) {
log.debug("Use Graphic.G0")
}
@@ -189,7 +205,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
}
ControlCharacters.SO -> {
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G1)
terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G1)
if (log.isDebugEnabled) {
log.debug("Use Graphic.G1")
}

View File

@@ -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
@@ -59,7 +61,6 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
init {
border = FlatRoundBorder()
isOpaque = false
isFocusable = false
isFloatable = false
isVisible = false
@@ -96,7 +97,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
}
}
if (isVisible == true) {
if (isVisible) {
isVisible = false
firePropertyChange("visible", true, false)
}
@@ -109,6 +110,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// 服务器信息
add(initServerInfoActionButton())
// Snippet
add(initSnippetActionButton())
// Nvidia 显卡信息
add(initNvidiaSMIActionButton())
@@ -147,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")
@@ -195,6 +217,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
private fun initCloseActionButton(): JButton {
val btn = JButton(Icons.closeSmall)
btn.toolTipText = I18n.getString("termora.floating-toolbar.close-in-current-tab")
btn.pressedIcon = Icons.closeSmallHovered
btn.rolloverIcon = Icons.closeSmallHovered
btn.addActionListener {

View File

@@ -156,6 +156,18 @@ class FileTransportPanel(
deleteAll.isEnabled = transportManager.getTransports().isNotEmpty()
}
popupMenu.addSeparator()
popupMenu.add(I18n.getString("termora.transport.jobs.table.status")).addActionListener {
val last = transports.last()
OptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor(this),
if (last.state == TransportState.Failed && last.stateText.isNotBlank()) last.stateText
else tableModel.formatStatus(last.state),
messageType = if (last.state == TransportState.Failed) JOptionPane.ERROR_MESSAGE else JOptionPane.INFORMATION_MESSAGE
)
}
popupMenu.show(table, event.x, event.y)
}

View File

@@ -96,7 +96,7 @@ class FileTransportTableModel(transportManager: TransportManager) : DefaultTable
}
}
private fun formatStatus(state: TransportState): String {
fun formatStatus(state: TransportState): String {
return when (state) {
TransportState.Transporting -> I18n.getString("termora.transport.sftp.status.transporting")
TransportState.Waiting -> I18n.getString("termora.transport.sftp.status.waiting")

View File

@@ -3,6 +3,8 @@ package app.termora.transport
import app.termora.Disposable
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.ObjectUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.commons.net.io.CopyStreamEvent
import org.apache.commons.net.io.CopyStreamListener
import org.apache.commons.net.io.Util
@@ -47,6 +49,7 @@ abstract class Transport(
field = value
listeners.forEach { it.onTransportChanged(this) }
}
var stateText: String = StringUtils.EMPTY
// 0 - 1
var progress = 0.0
@@ -186,6 +189,7 @@ class FileTransport(
log.error(e.message, e)
}
state = TransportState.Failed
stateText = ExceptionUtils.getRootCauseMessage(e)
} finally {
counter.clear()
}

View File

@@ -17,7 +17,7 @@ termora.quit-confirm=Quit {0}?
# update
termora.update.title=New version
termora.update.update=Update
termora.update.ignore=Ignore this version
termora.update.ignore=Remind me next time
# Doorman
termora.doorman.safe=Data is encrypted
@@ -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
@@ -364,6 +368,7 @@ termora.visual-window.nvidia-smi=NVIDIA SMI
termora.floating-toolbar.not-supported=This action is not supported
termora.floating-toolbar.close-in-current-tab=Close in current tab
# zmodem

View File

@@ -15,7 +15,7 @@ termora.quit-confirm=你要退出 {0} 吗?
# update
termora.update.title=新版本
termora.update.update=更新
termora.update.ignore=忽略这个版本
termora.update.ignore=下次提醒我
# Doorman
termora.doorman.safe=数据已加密
@@ -245,6 +245,11 @@ termora.macro.manager=管理宏
termora.macro.run=运行
# Snippets
termora.snippet=片段
termora.snippet.title=代码片段
# Transport
termora.transport.local=本机
@@ -346,6 +351,7 @@ termora.visual-window.system-information.filesystem=文件系统
termora.visual-window.system-information.used-total=使用 / 大小
termora.floating-toolbar.not-supported=不允许此操作
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭
# zmodem

View File

@@ -14,7 +14,7 @@ termora.quit-confirm=你要退出 {0} 嗎?
# update
termora.update.title=新版本
termora.update.update=更新
termora.update.ignore=忽略這個版本
termora.update.ignore=下次提醒我
@@ -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=父資料夾
@@ -325,6 +332,7 @@ termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小
termora.floating-toolbar.not-supported=不允許此操作
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
# zmodem
termora.addons.zmodem.skip=跳過

View File

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

After

Width:  |  Height:  |  Size: 681 B

View File

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

After

Width:  |  Height:  |  Size: 681 B

View File

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

After

Width:  |  Height:  |  Size: 791 B

View File

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

After

Width:  |  Height:  |  Size: 791 B

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB