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 fetch-depth: 0
# download jdk # 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 # appimagetool
- run: sudo apt install libfuse2 - run: sudo apt install libfuse2

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
# download jdk # 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 # appimagetool
- run: sudo apt install libfuse2 - 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 import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary Information - name: Setup the Notary information
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_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" xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk # 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 # install jdk
- name: Installing Java - name: Installing Java
@@ -70,7 +70,7 @@ jobs:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }} TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }} 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 }} TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: | run: |
./gradlew dist --no-daemon ./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 import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary Information - name: Setup the Notary information
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_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" xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk # 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 # install jdk
- name: Installing Java - name: Installing Java
@@ -72,7 +72,7 @@ jobs:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }} TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }} 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 }} TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: | run: |
./gradlew dist --no-daemon ./gradlew dist --no-daemon

View File

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

View File

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

View File

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

View File

@@ -94,6 +94,9 @@ object Icons {
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") } val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") } val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") } val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
val anyType by lazy { DynamicIcon("icons/anyType.svg", "icons/anyType_dark.svg") }
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") } val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") } val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
val applyNotConflictsLeft by lazy { val applyNotConflictsLeft by lazy {

View File

@@ -10,6 +10,7 @@ import java.awt.event.*
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.plaf.TabbedPaneUI
import kotlin.math.abs import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() { class MyTabbedPane : FlatTabbedPane() {
@@ -21,11 +22,18 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame .getData(DataProviders.TermoraFrame) as TermoraFrame
private val myUI = MyFlatTabbedPaneUI()
init { init {
isFocusable = false
super.setUI(myUI)
initEvents() initEvents()
} }
override fun setUI(ui: TabbedPaneUI?) {
super.setUI(myUI)
}
override fun updateUI() { override fun updateUI() {
styleMap = mapOf( styleMap = mapOf(
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"), "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.actions.OpenHostAction
import app.termora.transport.SFTPAction import app.termora.transport.SFTPAction
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVFormat
@@ -19,46 +17,31 @@ import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.ini4j.Ini import org.ini4j.Ini
import org.ini4j.Reg import org.ini4j.Reg
import org.jdesktop.swingx.JXTree
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import java.awt.Component import java.awt.Component
import java.awt.Dimension import java.awt.event.*
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.io.* import java.io.*
import java.util.* import java.util.*
import java.util.function.Function import java.util.function.Function
import javax.swing.* import javax.swing.*
import javax.swing.event.CellEditorListener
import javax.swing.event.ChangeEvent
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import javax.swing.filechooser.FileNameExtensionFilter import javax.swing.filechooser.FileNameExtensionFilter
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel import javax.swing.tree.TreeSelectionModel
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
import kotlin.math.min
class NewHostTree : JXTree() { class NewHostTree : SimpleTree() {
companion object { companion object {
private val log = LoggerFactory.getLogger(NewHostTree::class.java) private val log = LoggerFactory.getLogger(NewHostTree::class.java)
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol") private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
} }
private val tree = this
private val editor = OutlineTextField(64)
private val hostManager get() = HostManager.getInstance() private val hostManager get() = HostManager.getInstance()
private val properties get() = Database.getDatabase().properties private val properties get() = Database.getDatabase().properties
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
@@ -67,7 +50,7 @@ class NewHostTree : JXTree() {
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean() get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
set(value) = properties.putString("HostTree.showMoreInfo", value.toString()) set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
private val model = NewHostTreeModel() override val model = NewHostTreeModel()
/** /**
* 是否允许显示右键菜单 * 是否允许显示右键菜单
@@ -92,7 +75,6 @@ class NewHostTree : JXTree() {
isRootVisible = true isRootVisible = true
dropMode = DropMode.ON_OR_INSERT dropMode = DropMode.ON_OR_INSERT
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
editor.preferredSize = Dimension(220, 0)
// renderer // renderer
setCellRenderer(object : DefaultXTreeCellRenderer() { setCellRenderer(object : DefaultXTreeCellRenderer() {
@@ -138,74 +120,16 @@ class NewHostTree : JXTree() {
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus) val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
icon = when (host.protocol) { icon = node.getIcon(sel, expanded, hasFocus)
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
Protocol.Serial -> if (sel && tree.hasFocus()) Icons.plugin.dark else Icons.plugin
else -> if (sel && tree.hasFocus()) Icons.terminal.dark else Icons.terminal
}
return c return c
} }
}) })
// rename
setCellEditor(object : DefaultCellEditor(editor) {
override fun isCellEditable(e: EventObject?): Boolean {
if (e is MouseEvent) {
return false
}
return super.isCellEditable(e)
}
override fun getCellEditorValue(): Any {
val node = lastSelectedPathComponent as HostTreeNode
return node.host
}
})
} }
private fun initEvents() { private fun initEvents() {
// 右键选中 // double click
addMouseListener(object : MouseAdapter() { addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (!SwingUtilities.isRightMouseButton(e)) {
return
}
requestFocusInWindow()
val selectionRows = selectionModel.selectionRows
val selRow = getClosestRowForLocation(e.x, e.y)
if (selRow < 0) {
selectionModel.clearSelection()
return
} else if (selectionRows != null && selectionRows.contains(selRow)) {
return
}
selectionPath = getPathForLocation(e.x, e.y)
setSelectionRow(selRow)
}
})
// contextmenu
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (!(SwingUtilities.isRightMouseButton(e))) {
return
}
if (Objects.isNull(lastSelectedPathComponent)) {
return
}
if (contextmenu) {
SwingUtilities.invokeLater { showContextmenu(e) }
}
}
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
@@ -216,145 +140,36 @@ class NewHostTree : JXTree() {
} }
}) })
// rename addKeyListener(object : KeyAdapter() {
getCellEditor().addCellEditorListener(object : CellEditorListener { override fun keyPressed(e: KeyEvent) {
override fun editingStopped(e: ChangeEvent) { if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
val lastHost = lastSelectedPathComponent val nodes = getSelectionSimpleTreeNodes()
if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) { if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
return 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 val lastNode = lastSelectedPathComponent
if (lastNode !is HostTreeNode) return if (lastNode !is HostTreeNode) return
val nodes = getSelectionSimpleTreeNodes()
val fullNodes = getSelectionSimpleTreeNodes(true)
val lastNodeParent = lastNode.parent ?: model.root val lastNodeParent = lastNode.parent ?: model.root
val lastHost = lastNode.host val lastHost = lastNode.host
@@ -426,7 +241,6 @@ class NewHostTree : JXTree() {
} }
remove.addActionListener(object : ActionListener { remove.addActionListener(object : ActionListener {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val nodes = getSelectionHostTreeNodes()
if (nodes.isEmpty()) return if (nodes.isEmpty()) return
if (OptionPane.showConfirmDialog( if (OptionPane.showConfirmDialog(
SwingUtilities.getWindowAncestor(tree), SwingUtilities.getWindowAncestor(tree),
@@ -453,7 +267,7 @@ class NewHostTree : JXTree() {
} }
}) })
copy.addActionListener { copy.addActionListener {
for (c in getSelectionHostTreeNodes()) { for (c in nodes) {
val p = c.parent ?: continue val p = c.parent ?: continue
val newNode = copyNode(c, p.host.id) val newNode = copyNode(c, p.host.id)
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1) model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
@@ -462,12 +276,12 @@ class NewHostTree : JXTree() {
} }
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) } rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
expandAll.addActionListener { expandAll.addActionListener {
for (node in getSelectionHostTreeNodes(true)) { for (node in fullNodes) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(model.getPathToRoot(node)))
} }
} }
colspanAll.addActionListener { colspanAll.addActionListener {
for (node in getSelectionHostTreeNodes(true).reversed()) { for (node in fullNodes.reversed()) {
collapsePath(TreePath(model.getPathToRoot(node))) collapsePath(TreePath(model.getPathToRoot(node)))
} }
} }
@@ -495,29 +309,10 @@ class NewHostTree : JXTree() {
model.nodeStructureChanged(lastNode) model.nodeStructureChanged(lastNode)
} }
}) })
refresh.addActionListener { refresh.addActionListener { refreshNode(lastNode) }
val expanded = mutableSetOf(lastNode.host.id)
for (e in lastNode.getAllChildren()) {
if (e.host.protocol == Protocol.Folder && isExpanded(TreePath(model.getPathToRoot(e)))) {
expanded.add(e.host.id)
}
}
// 刷新
model.reload(lastNode)
// 先展开最顶级的
expandPath(TreePath(model.getPathToRoot(lastNode)))
for (child in lastNode.getAllChildren()) {
if (expanded.contains(child.host.id)) {
expandPath(TreePath(model.getPathToRoot(child)))
}
}
}
newMenu.isEnabled = lastHost.protocol == Protocol.Folder newMenu.isEnabled = lastHost.protocol == Protocol.Folder
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root } remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
copy.isEnabled = remove.isEnabled copy.isEnabled = remove.isEnabled
rename.isEnabled = remove.isEnabled rename.isEnabled = remove.isEnabled
property.isEnabled = lastHost.protocol != Protocol.Folder property.isEnabled = lastHost.protocol != Protocol.Folder
@@ -525,28 +320,27 @@ class NewHostTree : JXTree() {
importMenu.isEnabled = lastHost.protocol == Protocol.Folder importMenu.isEnabled = lastHost.protocol == Protocol.Folder
// 如果选中了 SSH 服务器,那么才启用 // 如果选中了 SSH 服务器,那么才启用
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH } openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled } openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
popupMenu.show(this, evt.x, evt.y)
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
tree.grabFocus()
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
tree.requestFocusInWindow()
}
override fun popupMenuCanceled(e: PopupMenuEvent) {
}
})
popupMenu.show(this, event.x, event.y)
} }
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
val lastNode = node as? HostTreeNode ?: return
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
model.nodeStructureChanged(lastNode)
hostManager.addHost(lastNode.host)
}
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
val nNode = node as? HostTreeNode ?: return
val nParent = parent as? HostTreeNode ?: return
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
hostManager.addHost(nNode.host)
}
private fun copyNode( private fun copyNode(
node: HostTreeNode, node: HostTreeNode,
parentId: String, parentId: String,
@@ -583,30 +377,14 @@ class NewHostTree : JXTree() {
} }
/** override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
* 包含孙子 return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
*/
fun getSelectionHostTreeNodes(include: Boolean = false): List<HostTreeNode> {
val paths = selectionPaths ?: return emptyList()
if (paths.isEmpty()) return emptyList()
val nodes = mutableListOf<HostTreeNode>()
val parents = paths.mapNotNull { it.lastPathComponent }
.filterIsInstance<HostTreeNode>().toMutableList()
if (include) {
while (parents.isNotEmpty()) {
val node = parents.removeFirst()
nodes.add(node)
parents.addAll(node.children().toList().filterIsInstance<HostTreeNode>())
}
}
return if (include) nodes else parents
} }
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) { private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
assertEventDispatchThread() assertEventDispatchThread()
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder } val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
if (nodes.isEmpty()) return if (nodes.isEmpty()) return
val source = if (openInNewWindow) val source = if (openInNewWindow)
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true } TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
@@ -615,7 +393,7 @@ class NewHostTree : JXTree() {
} }
private fun openWithSFTP(evt: EventObject) { private fun openWithSFTP(evt: EventObject) {
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH } val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
if (nodes.isEmpty()) return if (nodes.isEmpty()) return
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return
@@ -626,7 +404,7 @@ class NewHostTree : JXTree() {
} }
private fun openWithSFTPCommand(evt: EventObject) { private fun openWithSFTPCommand(evt: EventObject) {
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH } val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
if (nodes.isEmpty()) return if (nodes.isEmpty()) return
for (host in nodes) { for (host in nodes) {
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt)) openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
@@ -1089,28 +867,5 @@ class NewHostTree : JXTree() {
electerm, electerm,
} }
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {
companion object {
val dataFlavor =
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
}
override fun getTransferDataFlavors(): Array<DataFlavor> {
return arrayOf(dataFlavor)
}
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
return dataFlavor == flavor
}
override fun getTransferData(flavor: DataFlavor?): Any {
if (flavor == dataFlavor) {
return nodes
}
throw UnsupportedFlavorException(flavor)
}
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,21 +78,14 @@ class TerminalTabbed(
tabs[oldIndex].onLostFocus() tabs[oldIndex].onLostFocus()
} }
tabbedPane.getComponentAt(newIndex).requestFocusInWindow()
if (newIndex >= 0 && tabs.size > newIndex) { if (newIndex >= 0 && tabs.size > newIndex) {
tabs[newIndex].onGrabFocus() 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() { tabbedPane.addMouseListener(object : MouseAdapter() {

View File

@@ -2,10 +2,13 @@ package app.termora
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Frame
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.UIManager
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
import kotlin.math.max
import kotlin.system.exitProcess import kotlin.system.exitProcess
class TermoraFrameManager { class TermoraFrameManager {
@@ -20,16 +23,32 @@ class TermoraFrameManager {
} }
private val frames = mutableListOf<TermoraFrame>() private val frames = mutableListOf<TermoraFrame>()
private val properties get() = Database.getDatabase().properties
fun createWindow(): TermoraFrame { fun createWindow(): TermoraFrame {
val frame = TermoraFrame() val frame = TermoraFrame().apply { registerCloseCallback(this) }
registerCloseCallback(frame)
frame.title = if (SystemInfo.isLinux) null else Application.getName() frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null) val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
frames.add(frame) if (rectangle.isMaximized) {
return frame 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> { fun getWindows(): Array<TermoraFrame> {
@@ -41,6 +60,9 @@ class TermoraFrameManager {
window.addWindowListener(object : WindowAdapter() { window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
// 存储位置信息
saveFrameRectangle(window)
// 删除 // 删除
frames.remove(window) frames.remove(window)
@@ -87,4 +109,27 @@ class TermoraFrameManager {
exitProcess(0) 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.AnActionEvent
import app.termora.actions.SettingsAction import app.termora.actions.SettingsAction
import app.termora.findeverywhere.FindEverywhereAction import app.termora.findeverywhere.FindEverywhereAction
import app.termora.snippet.SnippetAction
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.WindowDecorations import com.jetbrains.WindowDecorations
@@ -42,6 +43,7 @@ class TermoraToolBar(
*/ */
fun getAllActions(): List<ToolBarAction> { fun getAllActions(): List<ToolBarAction> {
return listOf( return listOf(
ToolBarAction(SnippetAction.SNIPPET, true),
ToolBarAction(Actions.SFTP, true), ToolBarAction(Actions.SFTP, true),
ToolBarAction(Actions.TERMINAL_LOGGER, true), ToolBarAction(Actions.TERMINAL_LOGGER, true),
ToolBarAction(Actions.MACRO, true), ToolBarAction(Actions.MACRO, true),

View File

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

View File

@@ -60,7 +60,6 @@ class UpdaterManager private constructor() {
val isSelf get() = this == self val isSelf get() = this == self
} }
private val properties get() = Database.getDatabase().properties
var lastVersion = LatestVersion.self var lastVersion = LatestVersion.self
fun fetchLatestVersion(): LatestVersion { fun fetchLatestVersion(): LatestVersion {
@@ -146,12 +145,4 @@ class UpdaterManager private constructor() {
return LatestVersion.self 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.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent 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 var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
private val dataProviderSupport = DataProviderSupport() private val dataProviderSupport = DataProviderSupport()
private val hostTreeModel = hostTree.model as NewHostTreeModel private val hostTreeModel = hostTree.model as NewHostTreeModel
private var lastFocused: Component? = null
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) { private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
searchTextField.text.isBlank() searchTextField.text.isBlank()
} }
@@ -258,6 +261,14 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
return false return false
} }
override fun onLostFocus() {
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
}
override fun onGrabFocus() {
SwingUtilities.invokeLater { lastFocused?.requestFocusInWindow() }
}
override fun dispose() { override fun dispose() {
properties.putString("WelcomeFullContent", fullContent.toString()) properties.putString("WelcomeFullContent", fullContent.toString())
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree)) 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.highlight.KeywordHighlightAction
import app.termora.keymgr.KeyManagerAction import app.termora.keymgr.KeyManagerAction
import app.termora.macro.MacroAction import app.termora.macro.MacroAction
import app.termora.snippet.SnippetAction
import app.termora.tlog.TerminalLoggerAction import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction import app.termora.transport.SFTPAction
import javax.swing.Action import javax.swing.Action
@@ -34,6 +35,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction()) addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, SFTPAction()) addAction(Actions.SFTP, SFTPAction())
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction()) addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
addAction(Actions.MACRO, MacroAction()) addAction(Actions.MACRO, MacroAction())
addAction(Actions.KEY_MANAGER, KeyManagerAction()) addAction(Actions.KEY_MANAGER, KeyManagerAction())

View File

@@ -47,6 +47,7 @@ class AppUpdateAction private constructor() : AnAction(
} }
private val updaterManager get() = UpdaterManager.getInstance() private val updaterManager get() = UpdaterManager.getInstance()
private var isRemindMeNextTime = false
init { init {
isEnabled = false isEnabled = false
@@ -65,7 +66,9 @@ class AppUpdateAction private constructor() : AnAction(
initialDelay = 3.minutes.inWholeMilliseconds, initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true 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 return
} }
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
try { try {
downloadLatestPkg(latestVersion) downloadLatestPkg(latestVersion)
} catch (e: Exception) { } catch (e: Exception) {
@@ -194,7 +193,7 @@ class AppUpdateAction private constructor() : AnAction(
return return
} else if (option == JOptionPane.NO_OPTION) { } else if (option == JOptionPane.NO_OPTION) {
isEnabled = false isEnabled = false
updaterManager.ignore(lastVersion.version) isRemindMeNextTime = true
} else if (option == JOptionPane.YES_OPTION) { } else if (option == JOptionPane.YES_OPTION) {
updateSelf(lastVersion) 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 { object DataProviders {
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class) val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
val Terminal = DataKey(app.termora.terminal.Terminal::class) val Terminal = DataKey(app.termora.terminal.Terminal::class)
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class) val PtyConnector get() = DataKey.PtyConnector
val TabbedPane = DataKey(app.termora.MyTabbedPane::class) val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class) val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,6 +62,14 @@ abstract class GitSyncer : SafetySyncer() {
} }
} }
// decode Snippets
if (config.ranges.contains(SyncRange.Snippets)) {
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
decodeSnippets(it.content, config)
}
}
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled") log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
} }
@@ -84,6 +92,16 @@ abstract class GitSyncer : SafetySyncer() {
gistFiles.add(GistFile("Hosts", hostsContent)) gistFiles.add(GistFile("Hosts", hostsContent))
} }
// Snippets
if (config.ranges.contains(SyncRange.Snippets)) {
val snippetsContent = encodeSnippets(key)
if (log.isDebugEnabled) {
log.debug("Push encryptedSnippets: {}", snippetsContent)
}
gistFiles.add(GistFile("Snippets", snippetsContent))
}
// KeyPairs // KeyPairs
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
val keysContent = encodeKeys(key) val keysContent = encodeKeys(key)

View File

@@ -14,7 +14,8 @@ import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import kotlinx.serialization.encodeToString import app.termora.snippet.Snippet
import app.termora.snippet.SnippetManager
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -32,6 +33,7 @@ abstract class SafetySyncer : Syncer {
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
protected val macroManager get() = MacroManager.getInstance() protected val macroManager get() = MacroManager.getInstance()
protected val keymapManager get() = KeymapManager.getInstance() protected val keymapManager get() = KeymapManager.getInstance()
protected val snippetManager get() = SnippetManager.getInstance()
protected fun decodeHosts(text: String, config: SyncConfig) { protected fun decodeHosts(text: String, config: SyncConfig) {
// aes key // aes key
@@ -131,6 +133,61 @@ abstract class SafetySyncer : Syncer {
} }
protected fun decodeSnippets(text: String, config: SyncConfig) {
// aes key
val key = getKey(config)
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
val snippets = snippetManager.snippets().associateBy { it.id }
for (encryptedSnippet in encryptedSnippets) {
val oldHost = snippets[encryptedSnippet.id]
// 如果一样,则无需配置
if (oldHost != null) {
if (oldHost.updateDate == encryptedSnippet.updateDate) {
continue
}
}
try {
// aes iv
val iv = getIv(encryptedSnippet.id)
val snippet = encryptedSnippet.copy(
name = encryptedSnippet.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
parentId = encryptedSnippet.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
snippet = encryptedSnippet.snippet.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
)
SwingUtilities.invokeLater { snippetManager.addSnippet(snippet) }
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn("Decode snippet: ${encryptedSnippet.id} failed. error: {}", e.message, e)
}
}
}
if (log.isDebugEnabled) {
log.debug("Decode hosts: {}", text)
}
}
protected fun encodeSnippets(key: ByteArray): String {
val snippets = mutableListOf<Snippet>()
for (snippet in snippetManager.snippets()) {
// aes iv
val iv = ArrayUtils.subarray(snippet.id.padEnd(16, '0').toByteArray(), 0, 16)
snippets.add(
snippet.copy(
name = snippet.name.aesCBCEncrypt(key, iv).encodeBase64String(),
snippet = snippet.snippet.aesCBCEncrypt(key, iv).encodeBase64String(),
parentId = snippet.parentId.aesCBCEncrypt(key, iv).encodeBase64String(),
)
)
}
return ohMyJson.encodeToString(snippets)
}
protected fun decodeKeys(text: String, config: SyncConfig) { protected fun decodeKeys(text: String, config: SyncConfig) {
// aes key // aes key
val key = getKey(config) val key = getKey(config)

View File

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

View File

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

View File

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

View File

@@ -171,7 +171,7 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
} }
} else { } else {
val x = terminal.getCursorModel().getPosition().x val x = terminal.getCursorModel().getPosition().x
terminal.getTabulator().setTabStop(x) terminal.getTabulator().setTabStop(x - 1)
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Horizontal Tab Set (HTS). col: $x") 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_F2), encode = "${ControlCharacters.ESC}OQ")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F3), encode = "${ControlCharacters.ESC}OR") 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_F4), encode = "${ControlCharacters.ESC}OS")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F5), encode = "${ControlCharacters.ESC}[15~"); 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_F6), encode = "${ControlCharacters.ESC}[17~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F7), encode = "${ControlCharacters.ESC}[18~"); 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_F8), encode = "${ControlCharacters.ESC}[19~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F9), encode = "${ControlCharacters.ESC}[20~"); 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_F10), encode = "${ControlCharacters.ESC}[21~")
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F11), encode = "${ControlCharacters.ESC}[23~"); 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_F12), encode = "${ControlCharacters.ESC}[24~")
terminal.getTerminalModel().addDataListener(object : DataListener { terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) { 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 { 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 { override fun getTerminal(): Terminal {
@@ -84,6 +117,91 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
mapping[event] = encode 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() { fun arrowKeysApplicationSequences() {
// Up // Up
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA") putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")

View File

@@ -1,7 +1,7 @@
package app.termora.terminal package app.termora.terminal
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import kotlin.math.min import kotlin.math.max
open class VisualTerminal : Terminal { open class VisualTerminal : Terminal {
@@ -119,6 +119,9 @@ open class VisualTerminal : Terminal {
private class MyProcessor(private val terminal: Terminal, reader: TerminalReader) { private class MyProcessor(private val terminal: Terminal, reader: TerminalReader) {
private var state: ProcessorState = TerminalState.READY 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 { companion object {
private val log = LoggerFactory.getLogger(MyProcessor::class.java) 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) { fun process(ch: Char) {
if (log.isTraceEnabled) { if (log.isTraceEnabled) {
val position = terminal.getCursorModel().getPosition() val position = cursorModel.getPosition()
log.trace("process [${printChar(ch)}] , state: $state , position: $position") log.trace("process [${printChar(ch)}] , state: $state , position: $position")
} }
@@ -155,16 +158,29 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
} }
ControlCharacters.CR -> { ControlCharacters.CR -> {
terminal.getCursorModel().move(CursorMove.RowHome) cursorModel.move(CursorMove.RowHome)
TerminalState.READY TerminalState.READY
} }
ControlCharacters.TAB -> { ControlCharacters.TAB -> {
val position = terminal.getCursorModel().getPosition() val position = cursorModel.getPosition()
// Next tab + 1如果当前 x = 11那么下一个就是 16因为在 TerminalLineBuffer#writeTerminalLineChar 的时候会 - 1 会导致错乱一位 // Next tab + 1如果当前 x = 11那么下一个就是 16因为在 TerminalLineBuffer#writeTerminalLineChar 的时候会 - 1 会导致错乱一位
var nextTab = terminal.getTabulator().nextTab(position.x) + 1 val nextTab = terminal.getTabulator().nextTab(position.x - 1) + 1
nextTab = min(terminal.getTerminalModel().getCols(), nextTab) val length = if (terminalModel.isAlternateScreenBuffer()) {
terminal.getCursorModel().move(row = position.y, col = nextTab) 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 TerminalState.READY
} }
@@ -176,12 +192,12 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
} }
ControlCharacters.BS -> { ControlCharacters.BS -> {
terminal.getCursorModel().move(CursorMove.Left) cursorModel.move(CursorMove.Left)
TerminalState.READY TerminalState.READY
} }
ControlCharacters.SI -> { ControlCharacters.SI -> {
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G0) terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G0)
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Use Graphic.G0") log.debug("Use Graphic.G0")
} }
@@ -189,7 +205,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
} }
ControlCharacters.SO -> { ControlCharacters.SO -> {
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G1) terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G1)
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Use Graphic.G1") 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.AnActionEvent
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.snippet.SnippetAction
import app.termora.snippet.SnippetTreeDialog
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
import app.termora.terminal.panel.vw.SystemInformationVisualWindow import app.termora.terminal.panel.vw.SystemInformationVisualWindow
@@ -59,7 +61,6 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
init { init {
border = FlatRoundBorder() border = FlatRoundBorder()
isOpaque = false
isFocusable = false isFocusable = false
isFloatable = false isFloatable = false
isVisible = false isVisible = false
@@ -96,7 +97,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
} }
} }
if (isVisible == true) { if (isVisible) {
isVisible = false isVisible = false
firePropertyChange("visible", true, false) firePropertyChange("visible", true, false)
} }
@@ -109,6 +110,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// 服务器信息 // 服务器信息
add(initServerInfoActionButton()) add(initServerInfoActionButton())
// Snippet
add(initSnippetActionButton())
// Nvidia 显卡信息 // Nvidia 显卡信息
add(initNvidiaSMIActionButton()) add(initNvidiaSMIActionButton())
@@ -147,6 +151,24 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
return btn return btn
} }
private fun initSnippetActionButton(): JButton {
val btn = JButton(Icons.codeSpan)
btn.toolTipText = I18n.getString("termora.snippet.title")
btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return
val terminal = tab.getData(DataProviders.Terminal) ?: return
val dialog = SnippetTreeDialog(evt.window)
dialog.setLocationRelativeTo(btn)
dialog.setLocation(dialog.x, btn.locationOnScreen.y + height + 2)
dialog.isVisible = true
val node = dialog.getSelectedNode() ?: return
SnippetAction.getInstance().runSnippet(node.data, terminal)
}
})
return btn
}
private fun initNvidiaSMIActionButton(): JButton { private fun initNvidiaSMIActionButton(): JButton {
val btn = JButton(Icons.nvidia) val btn = JButton(Icons.nvidia)
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi") btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
@@ -195,6 +217,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
private fun initCloseActionButton(): JButton { private fun initCloseActionButton(): JButton {
val btn = JButton(Icons.closeSmall) val btn = JButton(Icons.closeSmall)
btn.toolTipText = I18n.getString("termora.floating-toolbar.close-in-current-tab")
btn.pressedIcon = Icons.closeSmallHovered btn.pressedIcon = Icons.closeSmallHovered
btn.rolloverIcon = Icons.closeSmallHovered btn.rolloverIcon = Icons.closeSmallHovered
btn.addActionListener { btn.addActionListener {

View File

@@ -156,6 +156,18 @@ class FileTransportPanel(
deleteAll.isEnabled = transportManager.getTransports().isNotEmpty() 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) 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) { return when (state) {
TransportState.Transporting -> I18n.getString("termora.transport.sftp.status.transporting") TransportState.Transporting -> I18n.getString("termora.transport.sftp.status.transporting")
TransportState.Waiting -> I18n.getString("termora.transport.sftp.status.waiting") TransportState.Waiting -> I18n.getString("termora.transport.sftp.status.waiting")

View File

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

View File

@@ -17,7 +17,7 @@ termora.quit-confirm=Quit {0}?
# update # update
termora.update.title=New version termora.update.title=New version
termora.update.update=Update termora.update.update=Update
termora.update.ignore=Ignore this version termora.update.ignore=Remind me next time
# Doorman # Doorman
termora.doorman.safe=Data is encrypted termora.doorman.safe=Data is encrypted
@@ -251,6 +251,10 @@ termora.macro.playback=Playback
termora.macro.manager=Manage Macros termora.macro.manager=Manage Macros
termora.macro.run=Run termora.macro.run=Run
# Snippets
termora.snippet=Snippet
termora.snippet.title=Snippets
# Tools # Tools
termora.tools.multiple=Send commands to multiple sessions termora.tools.multiple=Send commands to multiple sessions
@@ -364,6 +368,7 @@ termora.visual-window.nvidia-smi=NVIDIA SMI
termora.floating-toolbar.not-supported=This action is not supported termora.floating-toolbar.not-supported=This action is not supported
termora.floating-toolbar.close-in-current-tab=Close in current tab
# zmodem # zmodem

View File

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

View File

@@ -14,7 +14,7 @@ termora.quit-confirm=你要退出 {0} 嗎?
# update # update
termora.update.title=新版本 termora.update.title=新版本
termora.update.update=更新 termora.update.update=更新
termora.update.ignore=忽略這個版本 termora.update.ignore=下次提醒我
@@ -239,6 +239,13 @@ termora.macro.playback=回放
termora.macro.manager=管理宏 termora.macro.manager=管理宏
termora.macro.run=運行 termora.macro.run=運行
# Snippets
termora.snippet=片段
termora.snippet.title=程式碼片段
# Transport # Transport
termora.transport.local=本機 termora.transport.local=本機
termora.transport.parent-folder=父資料夾 termora.transport.parent-folder=父資料夾
@@ -325,6 +332,7 @@ termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小 termora.visual-window.system-information.used-total=使用 / 大小
termora.floating-toolbar.not-supported=不允許此操作 termora.floating-toolbar.not-supported=不允許此操作
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
# zmodem # zmodem
termora.addons.zmodem.skip=跳過 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