mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422e9aac84 | ||
|
|
9915c373b7 | ||
|
|
eba85e6348 | ||
|
|
483a7772f4 | ||
|
|
dcc96358f6 | ||
|
|
b5c30d505b | ||
|
|
1f3ef5f3f0 | ||
|
|
d388bcfc92 | ||
|
|
562c1f98fe | ||
|
|
f3c5009a45 | ||
|
|
09a1d9f51e | ||
|
|
84b48278ad |
2
.github/workflows/linux-aarch64.yml
vendored
2
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b825.69.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
|
||||
2
.github/workflows/linux-x86-64.yml
vendored
2
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
|
||||
8
.github/workflows/osx-aarch64.yml
vendored
8
.github/workflows/osx-aarch64.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
8
.github/workflows/osx-x86-64.yml
vendored
8
.github/workflows/osx-x86-64.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary Information
|
||||
if: github.ref_type == 'tag' && github.repository == 'TermoraDev/termora'
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: ${{ github.ref_type == 'tag' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
@@ -20,7 +20,7 @@ plugins {
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
|
||||
139
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal file
139
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.sync.SyncType
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
@@ -12,7 +13,6 @@ import jetbrains.exodus.env.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -26,6 +26,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
companion object {
|
||||
private const val KEYMAP_STORE = "Keymap"
|
||||
private const val HOST_STORE = "Host"
|
||||
private const val SNIPPET_STORE = "Snippet"
|
||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||
private const val MACRO_STORE = "Macro"
|
||||
private const val KEY_PAIR_STORE = "KeyPair"
|
||||
@@ -105,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllHost() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
store.openCursor(tx).use {
|
||||
while (it.next) {
|
||||
it.deleteCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllKeyPair() {
|
||||
env.executeInTransaction { tx ->
|
||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||
@@ -152,15 +142,32 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHost(id: String) {
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
var text = ohMyJson.encodeToString(snippet)
|
||||
if (doorman.isWorking()) {
|
||||
text = doorman.encrypt(text)
|
||||
}
|
||||
env.executeInTransaction {
|
||||
delete(it, HOST_STORE, id)
|
||||
put(it, SNIPPET_STORE, snippet.id, text)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Removed Host: $id")
|
||||
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getSnippets(): Collection<Snippet> {
|
||||
val isWorking = doorman.isWorking()
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||
if (isWorking)
|
||||
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||
else
|
||||
ohMyJson.decodeFromString(value)
|
||||
}.values
|
||||
}
|
||||
}
|
||||
|
||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||
return env.computeInTransaction { tx ->
|
||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||
@@ -621,6 +628,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var rangeHosts by BooleanPropertyDelegate(true)
|
||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||
var rangeMacros by BooleanPropertyDelegate(true)
|
||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||
|
||||
@@ -41,7 +41,7 @@ class FilterableHostTreeModel(
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.host.protocol != Protocol.Folder) {
|
||||
if (c.data.protocol != Protocol.Folder) {
|
||||
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
||||
companion object {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
}
|
||||
|
||||
var host: Host
|
||||
get() = data
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
override val isFolder: Boolean
|
||||
get() = data.protocol == Protocol.Folder
|
||||
|
||||
override val id: String
|
||||
get() = data.id
|
||||
|
||||
/**
|
||||
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
|
||||
*/
|
||||
var host: Host
|
||||
override var data: Host
|
||||
get() {
|
||||
val cacheHost = hostManager.getHost((userObject as Host).id)
|
||||
val myHost = userObject as Host
|
||||
@@ -22,22 +34,23 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false }
|
||||
override val folderCount
|
||||
get() = children().toList().count { if (it is HostTreeNode) it.data.protocol == Protocol.Folder else false }
|
||||
|
||||
override fun getParent(): HostTreeNode? {
|
||||
return super.getParent() as HostTreeNode?
|
||||
}
|
||||
|
||||
fun getAllChildren(): List<HostTreeNode> {
|
||||
val children = mutableListOf<HostTreeNode>()
|
||||
for (child in children()) {
|
||||
if (child is HostTreeNode) {
|
||||
children.add(child)
|
||||
children.addAll(child.getAllChildren())
|
||||
}
|
||||
override fun getAllChildren(): List<HostTreeNode> {
|
||||
return super.getAllChildren().filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||
return when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
fun childrenNode(): List<HostTreeNode> {
|
||||
@@ -57,7 +70,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
|
||||
for (child in oldNode.childrenNode()) {
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue
|
||||
if (scopes.isNotEmpty() && !scopes.contains(child.data.protocol)) continue
|
||||
val newChildNode = child.clone() as HostTreeNode
|
||||
deepClone(newChildNode, child, scopes)
|
||||
newNode.add(newChildNode)
|
||||
@@ -65,7 +78,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
}
|
||||
|
||||
override fun clone(): Any {
|
||||
val newNode = HostTreeNode(host)
|
||||
val newNode = HostTreeNode(data)
|
||||
newNode.children = null
|
||||
newNode.parent = null
|
||||
return newNode
|
||||
@@ -74,7 +87,7 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
override fun isNodeChild(aNode: TreeNode?): Boolean {
|
||||
if (aNode is HostTreeNode) {
|
||||
for (node in childrenNode()) {
|
||||
if (node.host == aNode.host) {
|
||||
if (node.data == aNode.data) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -88,10 +101,10 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) {
|
||||
|
||||
other as HostTreeNode
|
||||
|
||||
return host == other.host
|
||||
return data == other.data
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return host.hashCode()
|
||||
return data.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ object Icons {
|
||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||
val anyType by lazy { DynamicIcon("icons/anyType.svg", "icons/anyType_dark.svg") }
|
||||
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
||||
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_dark.svg") }
|
||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||
val applyNotConflictsLeft by lazy {
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.awt.event.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.plaf.TabbedPaneUI
|
||||
import kotlin.math.abs
|
||||
|
||||
class MyTabbedPane : FlatTabbedPane() {
|
||||
@@ -21,11 +22,18 @@ class MyTabbedPane : FlatTabbedPane() {
|
||||
private val owner
|
||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||
private val myUI = MyFlatTabbedPaneUI()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
super.setUI(myUI)
|
||||
initEvents()
|
||||
}
|
||||
|
||||
override fun setUI(ui: TabbedPaneUI?) {
|
||||
super.setUI(myUI)
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
styleMap = mapOf(
|
||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||
|
||||
@@ -5,8 +5,6 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.csv.CSVFormat
|
||||
@@ -19,46 +17,31 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.ini4j.Ini
|
||||
import org.ini4j.Reg
|
||||
import org.jdesktop.swingx.JXTree
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
import javax.swing.tree.TreePath
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
import kotlin.math.min
|
||||
|
||||
class NewHostTree : JXTree() {
|
||||
class NewHostTree : SimpleTree() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
|
||||
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
|
||||
}
|
||||
|
||||
private val tree = this
|
||||
private val editor = OutlineTextField(64)
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
@@ -67,7 +50,7 @@ class NewHostTree : JXTree() {
|
||||
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||
|
||||
private val model = NewHostTreeModel()
|
||||
override val model = NewHostTreeModel()
|
||||
|
||||
/**
|
||||
* 是否允许显示右键菜单
|
||||
@@ -92,7 +75,6 @@ class NewHostTree : JXTree() {
|
||||
isRootVisible = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||
editor.preferredSize = Dimension(220, 0)
|
||||
|
||||
// renderer
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
@@ -138,74 +120,16 @@ class NewHostTree : JXTree() {
|
||||
|
||||
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||
|
||||
icon = when (host.protocol) {
|
||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
Protocol.Serial -> if (sel && tree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||
else -> if (sel && tree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||
}
|
||||
icon = node.getIcon(sel, expanded, hasFocus)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent) {
|
||||
return false
|
||||
}
|
||||
return super.isCellEditable(e)
|
||||
}
|
||||
|
||||
override fun getCellEditorValue(): Any {
|
||||
val node = lastSelectedPathComponent as HostTreeNode
|
||||
return node.host
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
// double click
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (contextmenu) {
|
||||
SwingUtilities.invokeLater { showContextmenu(e) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||
@@ -216,145 +140,36 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val lastHost = lastSelectedPathComponent
|
||||
if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) {
|
||||
return
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
|
||||
val path = TreePath(model.getPathToRoot(nodes.first()))
|
||||
if (isExpanded(path)) {
|
||||
collapsePath(path)
|
||||
} else {
|
||||
expandPath(path)
|
||||
}
|
||||
} else {
|
||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
lastHost.host = lastHost.host.copy(name = editor.text, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(lastHost.host)
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable? {
|
||||
val nodes = getSelectionHostTreeNodes().toMutableList()
|
||||
if (nodes.isEmpty()) return null
|
||||
if (nodes.contains(model.root)) return null
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveHostTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (support.component != tree) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) return false
|
||||
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||
if (nodes.isEmpty()) return false
|
||||
if (node.host.protocol != Protocol.Folder) return false
|
||||
|
||||
for (e in nodes) {
|
||||
// 禁止拖拽到自己的子下面
|
||||
if (dropLocation.path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(dropLocation.path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件夹只能拖拽到文件夹的下面
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
if (dropLocation.childIndex > node.folderCount) {
|
||||
return false
|
||||
}
|
||||
} else if (dropLocation.childIndex != -1) {
|
||||
// 非文件夹也不能拖拽到文件夹的上面
|
||||
if (dropLocation.childIndex < node.folderCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val p = e.parent ?: continue
|
||||
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||
if (p == node && dropLocation.childIndex != -1) {
|
||||
val idx = p.getIndex(e)
|
||||
if (dropLocation.childIndex in idx..idx + 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
support.setShowDropLocation(true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? HostTreeNode ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<HostTreeNode>() ?: return false
|
||||
|
||||
// 展开的 host id
|
||||
val expanded = mutableSetOf(node.host.id)
|
||||
for (e in nodes) {
|
||||
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||
.map { it.host.id }.forEach { expanded.add(it) }
|
||||
}
|
||||
|
||||
// 转移
|
||||
for (e in nodes) {
|
||||
model.removeNodeFromParent(e)
|
||||
e.host = e.host.copy(parentId = node.host.id, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(e.host)
|
||||
|
||||
if (dropLocation.childIndex == -1) {
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
model.insertNodeInto(e, node, node.folderCount)
|
||||
} else {
|
||||
model.insertNodeInto(e, node, node.childCount)
|
||||
}
|
||||
} else {
|
||||
if (e.host.protocol == Protocol.Folder) {
|
||||
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||
} else {
|
||||
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||
}
|
||||
}
|
||||
|
||||
selectionPath = TreePath(model.getPathToRoot(e))
|
||||
}
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
|
||||
for (child in node.getAllChildren()) {
|
||||
if (expanded.contains(child.host.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showContextmenu(event: MouseEvent) {
|
||||
override fun showContextmenu(evt: MouseEvent) {
|
||||
if (!contextmenu) return
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is HostTreeNode) return
|
||||
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
val fullNodes = getSelectionSimpleTreeNodes(true)
|
||||
val lastNodeParent = lastNode.parent ?: model.root
|
||||
val lastHost = lastNode.host
|
||||
|
||||
@@ -426,7 +241,6 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
remove.addActionListener(object : ActionListener {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val nodes = getSelectionHostTreeNodes()
|
||||
if (nodes.isEmpty()) return
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(tree),
|
||||
@@ -453,7 +267,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
})
|
||||
copy.addActionListener {
|
||||
for (c in getSelectionHostTreeNodes()) {
|
||||
for (c in nodes) {
|
||||
val p = c.parent ?: continue
|
||||
val newNode = copyNode(c, p.host.id)
|
||||
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
|
||||
@@ -462,12 +276,12 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||
expandAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true)) {
|
||||
for (node in fullNodes) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
colspanAll.addActionListener {
|
||||
for (node in getSelectionHostTreeNodes(true).reversed()) {
|
||||
for (node in fullNodes.reversed()) {
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
@@ -495,29 +309,10 @@ class NewHostTree : JXTree() {
|
||||
model.nodeStructureChanged(lastNode)
|
||||
}
|
||||
})
|
||||
refresh.addActionListener {
|
||||
val expanded = mutableSetOf(lastNode.host.id)
|
||||
for (e in lastNode.getAllChildren()) {
|
||||
if (e.host.protocol == Protocol.Folder && isExpanded(TreePath(model.getPathToRoot(e)))) {
|
||||
expanded.add(e.host.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
model.reload(lastNode)
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(lastNode)))
|
||||
|
||||
for (child in lastNode.getAllChildren()) {
|
||||
if (expanded.contains(child.host.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh.addActionListener { refreshNode(lastNode) }
|
||||
|
||||
newMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
remove.isEnabled = getSelectionHostTreeNodes().none { it == model.root }
|
||||
remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
|
||||
copy.isEnabled = remove.isEnabled
|
||||
rename.isEnabled = remove.isEnabled
|
||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||
@@ -525,28 +320,27 @@ class NewHostTree : JXTree() {
|
||||
importMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||
|
||||
// 如果选中了 SSH 服务器,那么才启用
|
||||
openWithSFTP.isEnabled = getSelectionHostTreeNodes(true).map { it.host }.any { it.protocol == Protocol.SSH }
|
||||
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
|
||||
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||
|
||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
tree.grabFocus()
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||
tree.requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
popupMenu.show(this, event.x, event.y)
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||
val lastNode = node as? HostTreeNode ?: return
|
||||
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
|
||||
model.nodeStructureChanged(lastNode)
|
||||
hostManager.addHost(lastNode.host)
|
||||
}
|
||||
|
||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
val nNode = node as? HostTreeNode ?: return
|
||||
val nParent = parent as? HostTreeNode ?: return
|
||||
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
|
||||
hostManager.addHost(nNode.host)
|
||||
}
|
||||
|
||||
|
||||
private fun copyNode(
|
||||
node: HostTreeNode,
|
||||
parentId: String,
|
||||
@@ -583,30 +377,14 @@ class NewHostTree : JXTree() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
fun getSelectionHostTreeNodes(include: Boolean = false): List<HostTreeNode> {
|
||||
val paths = selectionPaths ?: return emptyList()
|
||||
if (paths.isEmpty()) return emptyList()
|
||||
val nodes = mutableListOf<HostTreeNode>()
|
||||
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||
.filterIsInstance<HostTreeNode>().toMutableList()
|
||||
|
||||
if (include) {
|
||||
while (parents.isNotEmpty()) {
|
||||
val node = parents.removeFirst()
|
||||
nodes.add(node)
|
||||
parents.addAll(node.children().toList().filterIsInstance<HostTreeNode>())
|
||||
}
|
||||
}
|
||||
|
||||
return if (include) nodes else parents
|
||||
override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
|
||||
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
|
||||
}
|
||||
|
||||
|
||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||
assertEventDispatchThread()
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||
if (nodes.isEmpty()) return
|
||||
val source = if (openInNewWindow)
|
||||
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||
@@ -615,7 +393,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
private fun openWithSFTP(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
|
||||
val sftpAction = ActionManager.getInstance().getAction(app.termora.Actions.SFTP) as SFTPAction? ?: return
|
||||
@@ -626,7 +404,7 @@ class NewHostTree : JXTree() {
|
||||
}
|
||||
|
||||
private fun openWithSFTPCommand(evt: EventObject) {
|
||||
val nodes = getSelectionHostTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||
if (nodes.isEmpty()) return
|
||||
for (host in nodes) {
|
||||
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||
@@ -1089,28 +867,5 @@ class NewHostTree : JXTree() {
|
||||
electerm,
|
||||
}
|
||||
|
||||
private class MoveHostTransferable(val nodes: List<HostTreeNode>) : Transferable {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(dataFlavor)
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||
return dataFlavor == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||
if (flavor == dataFlavor) {
|
||||
return nodes
|
||||
}
|
||||
throw UnsupportedFlavorException(flavor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class NewHostTreeDialog(
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
hosts = tree.getSelectionHostTreeNodes(true)
|
||||
hosts = tree.getSelectionSimpleTreeNodes(true)
|
||||
.filter { filter.apply(it) }
|
||||
.map { it.host }
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
|
||||
class NewHostTreeModel : DefaultTreeModel(
|
||||
class NewHostTreeModel : SimpleTreeModel<Host>(
|
||||
HostTreeNode(
|
||||
Host(
|
||||
id = "0",
|
||||
|
||||
@@ -15,6 +15,8 @@ import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.native.FileChooser
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.snippet.SnippetManager
|
||||
import app.termora.sync.SyncConfig
|
||||
import app.termora.sync.SyncRange
|
||||
import app.termora.sync.SyncType
|
||||
@@ -67,6 +69,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||
private val database get() = Database.getDatabase()
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val macroManager get() = MacroManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
@@ -561,6 +564,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
val sync get() = database.sync
|
||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||
val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title"))
|
||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
||||
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||
@@ -665,6 +669,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
keysCheckBox.addActionListener { refreshButtons() }
|
||||
hostsCheckBox.addActionListener { refreshButtons() }
|
||||
snippetsCheckBox.addActionListener { refreshButtons() }
|
||||
keywordHighlightsCheckBox.addActionListener { refreshButtons() }
|
||||
|
||||
}
|
||||
@@ -672,6 +677,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun refreshButtons() {
|
||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||
sync.rangeHosts = hostsCheckBox.isSelected
|
||||
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||
|
||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||
@@ -848,6 +854,17 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.Snippets)) {
|
||||
val snippets = json["snippets"]
|
||||
if (snippets is JsonArray) {
|
||||
ohMyJson.runCatching { decodeFromJsonElement<List<Snippet>>(snippets.jsonArray) }.onSuccess {
|
||||
for (snippet in it) {
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keyPairs = json["keyPairs"]
|
||||
if (keyPairs is JsonArray) {
|
||||
@@ -909,6 +926,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.Snippets)) {
|
||||
put("snippets", ohMyJson.encodeToJsonElement(snippetManager.snippets()))
|
||||
}
|
||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||
}
|
||||
@@ -978,6 +998,9 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
if (keymapCheckBox.isSelected) {
|
||||
range.add(SyncRange.Keymap)
|
||||
}
|
||||
if (snippetsCheckBox.isSelected) {
|
||||
range.add(SyncRange.Snippets)
|
||||
}
|
||||
return SyncConfig(
|
||||
type = typeComboBox.selectedItem as SyncType,
|
||||
token = String(tokenTextField.password),
|
||||
@@ -1054,6 +1077,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
keymapCheckBox.isEnabled = false
|
||||
keywordHighlightsCheckBox.isEnabled = false
|
||||
hostsCheckBox.isEnabled = false
|
||||
snippetsCheckBox.isEnabled = false
|
||||
domainTextField.isEnabled = false
|
||||
|
||||
if (push) {
|
||||
@@ -1083,6 +1107,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
uploadConfigButton.isEnabled = true
|
||||
keysCheckBox.isEnabled = true
|
||||
hostsCheckBox.isEnabled = true
|
||||
snippetsCheckBox.isEnabled = true
|
||||
typeComboBox.isEnabled = true
|
||||
macrosCheckBox.isEnabled = true
|
||||
keymapCheckBox.isEnabled = true
|
||||
@@ -1144,12 +1169,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
typeComboBox.addItem(SyncType.WebDAV)
|
||||
|
||||
hostsCheckBox.isFocusable = false
|
||||
snippetsCheckBox.isFocusable = false
|
||||
keysCheckBox.isFocusable = false
|
||||
keywordHighlightsCheckBox.isFocusable = false
|
||||
macrosCheckBox.isFocusable = false
|
||||
keymapCheckBox.isFocusable = false
|
||||
|
||||
hostsCheckBox.isSelected = sync.rangeHosts
|
||||
snippetsCheckBox.isSelected = sync.rangeSnippets
|
||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||
macrosCheckBox.isSelected = sync.rangeMacros
|
||||
@@ -1236,7 +1263,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, left:pref, $formMargin, left:pref",
|
||||
"pref, $formMargin, pref"
|
||||
"pref, 2dlu, pref"
|
||||
)
|
||||
)
|
||||
.add(hostsCheckBox).xy(1, 1)
|
||||
@@ -1244,6 +1271,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(keywordHighlightsCheckBox).xy(5, 1)
|
||||
.add(macrosCheckBox).xy(1, 3)
|
||||
.add(keymapCheckBox).xy(3, 3)
|
||||
.add(snippetsCheckBox).xy(5, 3)
|
||||
.build()
|
||||
|
||||
var rows = 1
|
||||
@@ -1612,13 +1640,15 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
val hosts = hostManager.hosts()
|
||||
val keyPairs = keyManager.getOhKeyPairs()
|
||||
val snippets = snippetManager.snippets()
|
||||
|
||||
// 获取到安全的属性,如果设置密码那表示之前并未加密
|
||||
// 这里取出来之后重新存储加密
|
||||
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
|
||||
|
||||
val key = doorman.work(passwordTextField.password)
|
||||
|
||||
hosts.forEach { hostManager.addHost(it) }
|
||||
snippets.forEach { snippetManager.addSnippet(it) }
|
||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||
for (e in properties) {
|
||||
for ((k, v) in e.second) {
|
||||
|
||||
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
@@ -0,0 +1,343 @@
|
||||
package app.termora
|
||||
|
||||
import org.jdesktop.swingx.JXTree
|
||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.event.CellEditorListener
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.tree.TreePath
|
||||
import kotlin.math.min
|
||||
|
||||
open class SimpleTree : JXTree() {
|
||||
|
||||
protected open val model get() = super.getModel() as SimpleTreeModel<*>
|
||||
private val editor = OutlineTextField(64)
|
||||
protected val tree get() = this
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
|
||||
// renderer
|
||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||
override fun getTreeCellRendererComponent(
|
||||
tree: JTree,
|
||||
value: Any,
|
||||
sel: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean
|
||||
): Component {
|
||||
val node = value as SimpleTreeNode<*>
|
||||
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||
icon = node.getIcon(sel, expanded, hasFocus)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
// rename
|
||||
setCellEditor(object : DefaultCellEditor(editor) {
|
||||
override fun isCellEditable(e: EventObject?): Boolean {
|
||||
if (e is MouseEvent || !tree.isCellEditable(e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return super.isCellEditable(e).apply {
|
||||
if (this) {
|
||||
editor.preferredSize = Dimension(min(220, width - 64), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCellEditorValue(): Any? {
|
||||
return getLastSelectedPathNode()?.data
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
// 右键选中
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFocusInWindow()
|
||||
|
||||
val selectionRows = selectionModel.selectionRows
|
||||
|
||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||
if (selRow < 0) {
|
||||
selectionModel.clearSelection()
|
||||
return
|
||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionPath = getPathForLocation(e.x, e.y)
|
||||
|
||||
setSelectionRow(selRow)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// contextmenu
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
showContextmenu(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// rename
|
||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||
override fun editingStopped(e: ChangeEvent) {
|
||||
val node = getLastSelectedPathNode() ?: return
|
||||
if (editor.text.isBlank() || editor.text == node.toString()) {
|
||||
return
|
||||
}
|
||||
onRenamed(node, editor.text)
|
||||
}
|
||||
|
||||
override fun editingCanceled(e: ChangeEvent) {
|
||||
}
|
||||
})
|
||||
|
||||
// drag
|
||||
transferHandler = object : TransferHandler() {
|
||||
|
||||
override fun createTransferable(c: JComponent): Transferable? {
|
||||
val nodes = getSelectionSimpleTreeNodes().toMutableList()
|
||||
if (nodes.isEmpty()) return null
|
||||
if (nodes.contains(model.root)) return null
|
||||
|
||||
val iterator = nodes.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val node = iterator.next()
|
||||
val parents = model.getPathToRoot(node).filter { it != node }
|
||||
if (parents.any { nodes.contains(it) }) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return MoveNodeTransferable(nodes)
|
||||
}
|
||||
|
||||
override fun getSourceActions(c: JComponent?): Int {
|
||||
return MOVE
|
||||
}
|
||||
|
||||
override fun canImport(support: TransferSupport): Boolean {
|
||||
if (support.component != tree) return false
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val path = dropLocation.path ?: return false
|
||||
val node = path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
if (!support.isDataFlavorSupported(MoveNodeTransferable.dataFlavor)) return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
if (nodes.isEmpty()) return false
|
||||
if (!node.isFolder) return false
|
||||
|
||||
for (e in nodes) {
|
||||
// 禁止拖拽到自己的子下面
|
||||
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件夹只能拖拽到文件夹的下面
|
||||
if (e.isFolder) {
|
||||
if (dropLocation.childIndex > node.folderCount) {
|
||||
return false
|
||||
}
|
||||
} else if (dropLocation.childIndex != -1) {
|
||||
// 非文件夹也不能拖拽到文件夹的上面
|
||||
if (dropLocation.childIndex < node.folderCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val p = e.parent ?: continue
|
||||
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||
if (p == node && dropLocation.childIndex != -1) {
|
||||
val idx = p.getIndex(e)
|
||||
if (dropLocation.childIndex in idx..idx + 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
support.setShowDropLocation(true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||
|
||||
// 展开的 node
|
||||
val expanded = mutableSetOf(node.id)
|
||||
for (e in nodes) {
|
||||
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||
.map { it }.forEach { expanded.add(it.id) }
|
||||
}
|
||||
|
||||
// 转移
|
||||
for (e in nodes) {
|
||||
model.removeNodeFromParent(e)
|
||||
rebase(e, node)
|
||||
|
||||
if (dropLocation.childIndex == -1) {
|
||||
if (e.isFolder) {
|
||||
model.insertNodeInto(e, node, node.folderCount)
|
||||
} else {
|
||||
model.insertNodeInto(e, node, node.childCount)
|
||||
}
|
||||
} else {
|
||||
if (e.isFolder) {
|
||||
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||
} else {
|
||||
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||
}
|
||||
}
|
||||
|
||||
selectionPath = TreePath(model.getPathToRoot(e))
|
||||
}
|
||||
|
||||
// 先展开最顶级的
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
|
||||
for (child in node.getAllChildren()) {
|
||||
if (expanded.contains(child.id)) {
|
||||
expandPath(TreePath(model.getPathToRoot(child)))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun newFolder(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.folderCount)
|
||||
}
|
||||
|
||||
protected open fun newFile(newNode: SimpleTreeNode<*>): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
return newNode(newNode, lastNode.childCount)
|
||||
}
|
||||
|
||||
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
||||
val lastNode = lastSelectedPathComponent
|
||||
if (lastNode !is SimpleTreeNode<*>) return false
|
||||
model.insertNodeInto(newNode, lastNode, index)
|
||||
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||
startEditingAtPath(selectionPath)
|
||||
return true
|
||||
}
|
||||
|
||||
open fun getLastSelectedPathNode(): SimpleTreeNode<*>? {
|
||||
return lastSelectedPathComponent as? SimpleTreeNode<*>
|
||||
}
|
||||
|
||||
|
||||
protected open fun showContextmenu(evt: MouseEvent) {
|
||||
|
||||
}
|
||||
|
||||
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||
|
||||
protected open fun refreshNode(node: SimpleTreeNode<*>) {
|
||||
val state = TreeUtils.saveExpansionState(tree)
|
||||
val rows = selectionRows
|
||||
|
||||
model.reload(node)
|
||||
|
||||
TreeUtils.loadExpansionState(tree, state)
|
||||
|
||||
super.setSelectionRows(rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* 包含孙子
|
||||
*/
|
||||
open fun getSelectionSimpleTreeNodes(include: Boolean = false): List<SimpleTreeNode<*>> {
|
||||
val paths = selectionPaths ?: return emptyList()
|
||||
if (paths.isEmpty()) return emptyList()
|
||||
val nodes = mutableListOf<SimpleTreeNode<*>>()
|
||||
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||
.filterIsInstance<SimpleTreeNode<*>>().toMutableList()
|
||||
|
||||
if (include) {
|
||||
while (parents.isNotEmpty()) {
|
||||
val node = parents.removeFirst()
|
||||
nodes.add(node)
|
||||
parents.addAll(node.children().toList().filterIsInstance<SimpleTreeNode<*>>())
|
||||
}
|
||||
}
|
||||
|
||||
return if (include) nodes else parents
|
||||
}
|
||||
|
||||
protected open fun isCellEditable(e: EventObject?): Boolean {
|
||||
return getLastSelectedPathNode() != model.root
|
||||
}
|
||||
|
||||
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
|
||||
}
|
||||
|
||||
private class MoveNodeTransferable(val nodes: List<SimpleTreeNode<*>>) : Transferable {
|
||||
companion object {
|
||||
val dataFlavor =
|
||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveNodeTransferable::class.java.name}")
|
||||
}
|
||||
|
||||
|
||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||
return arrayOf(dataFlavor)
|
||||
}
|
||||
|
||||
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||
return dataFlavor == flavor
|
||||
}
|
||||
|
||||
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||
if (flavor == dataFlavor) {
|
||||
return nodes
|
||||
}
|
||||
throw UnsupportedFlavorException(flavor)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
|
||||
abstract class SimpleTreeModel<T>(root: SimpleTreeNode<T>) : DefaultTreeModel(root) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getRoot(): SimpleTreeNode<T> {
|
||||
return super.getRoot() as SimpleTreeNode<T>
|
||||
}
|
||||
|
||||
}
|
||||
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package app.termora
|
||||
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
|
||||
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open var data: T
|
||||
get() = userObject as T
|
||||
set(value) = setUserObject(value)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getParent(): SimpleTreeNode<T>? {
|
||||
return super.getParent() as SimpleTreeNode<T>?
|
||||
}
|
||||
|
||||
open val folderCount: Int get() = 0
|
||||
|
||||
open fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon? {
|
||||
return null
|
||||
}
|
||||
|
||||
open val isFolder get() = false
|
||||
|
||||
abstract val id: String
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun getAllChildren(): List<SimpleTreeNode<T>> {
|
||||
val children = mutableListOf<SimpleTreeNode<T>>()
|
||||
for (child in children()) {
|
||||
val c = child as? SimpleTreeNode<T> ?: continue
|
||||
children.add(c)
|
||||
children.addAll(c.getAllChildren())
|
||||
}
|
||||
return children
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface TerminalTab : Disposable {
|
||||
interface TerminalTab : Disposable, DataProvider {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
|
||||
@@ -78,21 +78,14 @@ class TerminalTabbed(
|
||||
tabs[oldIndex].onLostFocus()
|
||||
}
|
||||
|
||||
tabbedPane.getComponentAt(newIndex).requestFocusInWindow()
|
||||
|
||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||
tabs[newIndex].onGrabFocus()
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater { tabbedPane.getComponentAt(newIndex).requestFocusInWindow() }
|
||||
|
||||
}
|
||||
|
||||
// 选择变动
|
||||
tabbedPane.addChangeListener {
|
||||
if (tabbedPane.selectedIndex >= 0) {
|
||||
val c = tabbedPane.getComponentAt(tabbedPane.selectedIndex)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
||||
|
||||
@@ -2,10 +2,13 @@ package app.termora
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Frame
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
||||
import kotlin.math.max
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class TermoraFrameManager {
|
||||
@@ -20,16 +23,32 @@ class TermoraFrameManager {
|
||||
}
|
||||
|
||||
private val frames = mutableListOf<TermoraFrame>()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
fun createWindow(): TermoraFrame {
|
||||
val frame = TermoraFrame()
|
||||
registerCloseCallback(frame)
|
||||
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||
frame.setSize(1280, 800)
|
||||
frame.setLocationRelativeTo(null)
|
||||
frames.add(frame)
|
||||
return frame
|
||||
|
||||
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||
if (rectangle.isMaximized) {
|
||||
frame.setSize(1280, 800)
|
||||
frame.setLocationRelativeTo(null)
|
||||
frame.extendedState = rectangle.s
|
||||
} else {
|
||||
// 控制最小
|
||||
frame.setSize(
|
||||
max(rectangle.w, UIManager.getInt("Dialog.width") - 150),
|
||||
max(rectangle.h, UIManager.getInt("Dialog.height") - 100)
|
||||
)
|
||||
if (rectangle.x == -1 && rectangle.y == -1) {
|
||||
frame.setLocationRelativeTo(null)
|
||||
} else {
|
||||
frame.setLocation(max(rectangle.x, 0), max(rectangle.y, 0))
|
||||
}
|
||||
}
|
||||
|
||||
return frame.apply { frames.add(this) }
|
||||
}
|
||||
|
||||
fun getWindows(): Array<TermoraFrame> {
|
||||
@@ -41,6 +60,9 @@ class TermoraFrameManager {
|
||||
window.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
|
||||
// 存储位置信息
|
||||
saveFrameRectangle(window)
|
||||
|
||||
// 删除
|
||||
frames.remove(window)
|
||||
|
||||
@@ -87,4 +109,27 @@ class TermoraFrameManager {
|
||||
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun saveFrameRectangle(frame: TermoraFrame) {
|
||||
properties.putString("TermoraFrame.x", frame.x.toString())
|
||||
properties.putString("TermoraFrame.y", frame.y.toString())
|
||||
properties.putString("TermoraFrame.width", frame.width.toString())
|
||||
properties.putString("TermoraFrame.height", frame.height.toString())
|
||||
properties.putString("TermoraFrame.extendedState", frame.extendedState.toString())
|
||||
}
|
||||
|
||||
private fun getFrameRectangle(): FrameRectangle? {
|
||||
val x = properties.getString("TermoraFrame.x")?.toIntOrNull() ?: return null
|
||||
val y = properties.getString("TermoraFrame.y")?.toIntOrNull() ?: return null
|
||||
val w = properties.getString("TermoraFrame.width")?.toIntOrNull() ?: return null
|
||||
val h = properties.getString("TermoraFrame.height")?.toIntOrNull() ?: return null
|
||||
val s = properties.getString("TermoraFrame.extendedState")?.toIntOrNull() ?: return null
|
||||
return FrameRectangle(x, y, w, h, s)
|
||||
}
|
||||
|
||||
private data class FrameRectangle(
|
||||
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
||||
) {
|
||||
val isMaximized get() = (s and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.WindowDecorations
|
||||
@@ -42,6 +43,7 @@ class TermoraToolBar(
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.termora
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import javax.swing.JTree
|
||||
import javax.swing.tree.TreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
object TreeUtils {
|
||||
/**
|
||||
@@ -31,16 +31,6 @@ object TreeUtils {
|
||||
return nodes
|
||||
}
|
||||
|
||||
fun parents(node: TreeNode): List<Any> {
|
||||
val parents = mutableListOf<Any>()
|
||||
var p = node.parent
|
||||
while (p != null) {
|
||||
parents.add(p)
|
||||
p = p.parent
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
fun saveExpansionState(tree: JTree): String {
|
||||
val rows = mutableListOf<Int>()
|
||||
for (i in 0 until tree.rowCount) {
|
||||
@@ -63,15 +53,15 @@ object TreeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun expandAll(tree: JTree) {
|
||||
var j = tree.rowCount
|
||||
var i = 0
|
||||
while (i < j) {
|
||||
tree.expandRow(i)
|
||||
i += 1
|
||||
j = tree.rowCount
|
||||
fun saveSelectionRows(tree: JTree): String {
|
||||
return tree.selectionRows?.joinToString(",") ?: StringUtils.EMPTY
|
||||
}
|
||||
|
||||
fun loadSelectionRows(tree: JTree, state: String) {
|
||||
if (state.isBlank()) return
|
||||
for (row in state.split(",").mapNotNull { it.toIntOrNull() }) {
|
||||
tree.addSelectionRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -60,7 +60,6 @@ class UpdaterManager private constructor() {
|
||||
val isSelf get() = this == self
|
||||
}
|
||||
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
var lastVersion = LatestVersion.self
|
||||
|
||||
fun fetchLatestVersion(): LatestVersion {
|
||||
@@ -146,12 +145,4 @@ class UpdaterManager private constructor() {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
|
||||
fun isIgnored(version: String): Boolean {
|
||||
return properties.getString("ignored.version.$version", "false").toBoolean()
|
||||
}
|
||||
|
||||
fun ignore(version: String) {
|
||||
properties.putString("ignored.version.$version", "true")
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
@@ -32,6 +34,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
||||
private var lastFocused: Component? = null
|
||||
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
||||
searchTextField.text.isBlank()
|
||||
}
|
||||
@@ -258,6 +261,14 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLostFocus() {
|
||||
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
}
|
||||
|
||||
override fun onGrabFocus() {
|
||||
SwingUtilities.invokeLater { lastFocused?.requestFocusInWindow() }
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.highlight.KeywordHighlightAction
|
||||
import app.termora.keymgr.KeyManagerAction
|
||||
import app.termora.macro.MacroAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import app.termora.tlog.TerminalLoggerAction
|
||||
import app.termora.transport.SFTPAction
|
||||
import javax.swing.Action
|
||||
@@ -34,6 +35,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, SFTPAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
private var isRemindMeNextTime = false
|
||||
|
||||
init {
|
||||
isEnabled = false
|
||||
@@ -65,7 +66,9 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
initialDelay = 3.minutes.inWholeMilliseconds,
|
||||
period = 5.hours.inWholeMilliseconds, daemon = true
|
||||
) {
|
||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
||||
if (!isRemindMeNextTime) {
|
||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +85,6 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
return
|
||||
}
|
||||
|
||||
if (updaterManager.isIgnored(latestVersion.version)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
downloadLatestPkg(latestVersion)
|
||||
} catch (e: Exception) {
|
||||
@@ -194,7 +193,7 @@ class AppUpdateAction private constructor() : AnAction(
|
||||
return
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
isEnabled = false
|
||||
updaterManager.ignore(lastVersion.version)
|
||||
isRemindMeNextTime = true
|
||||
} else if (option == JOptionPane.YES_OPTION) {
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ interface DataProvider {
|
||||
/**
|
||||
* 数据提供
|
||||
*/
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T?
|
||||
fun <T : Any> getData(dataKey: DataKey<T>): T? = null
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
|
||||
object DataProviders {
|
||||
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
||||
val PtyConnector get() = DataKey.PtyConnector
|
||||
|
||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.jdesktop.swingx.action.AbstractActionExt
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.Action
|
||||
import javax.swing.Icon
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
|
||||
private val isState: Boolean
|
||||
@@ -26,7 +27,7 @@ open class ActionFindEverywhereResult(private val action: Action) : FindEverywhe
|
||||
if (isState) {
|
||||
action.putValue(Action.SELECTED_KEY, !isSelected)
|
||||
}
|
||||
action.actionPerformed(e)
|
||||
SwingUtilities.invokeLater { action.actionPerformed(e) }
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenLocalTerminalAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import javax.swing.Icon
|
||||
@@ -21,6 +22,11 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// Snippet
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
// SFTP
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
|
||||
24
src/main/kotlin/app/termora/snippet/Snippet.kt
Normal file
24
src/main/kotlin/app/termora/snippet/Snippet.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.toSimpleString
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
enum class SnippetType {
|
||||
Folder,
|
||||
Snippet,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Snippet(
|
||||
val id: String = UUID.randomUUID().toSimpleString(),
|
||||
val name: String,
|
||||
val snippet: String = StringUtils.EMPTY,
|
||||
val parentId: String = StringUtils.EMPTY,
|
||||
val type: SnippetType = SnippetType.Snippet,
|
||||
val deleted: Boolean = false,
|
||||
val sort: Long = System.currentTimeMillis(),
|
||||
val createDate: Long = System.currentTimeMillis(),
|
||||
val updateDate: Long = System.currentTimeMillis(),
|
||||
)
|
||||
47
src/main/kotlin/app/termora/snippet/SnippetAction.kt
Normal file
47
src/main/kotlin/app/termora/snippet/SnippetAction.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.Terminal
|
||||
|
||||
class SnippetAction private constructor() : AnAction(I18n.getString("termora.snippet.title"), Icons.codeSpan) {
|
||||
companion object {
|
||||
fun getInstance(): SnippetAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(SnippetAction::class) { SnippetAction() }
|
||||
}
|
||||
|
||||
const val SNIPPET = "SnippetAction"
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
SnippetDialog(evt.window).isVisible = true
|
||||
}
|
||||
|
||||
|
||||
fun runSnippet(snippet: Snippet, terminal: Terminal) {
|
||||
if (snippet.type != SnippetType.Snippet) return
|
||||
val terminalModel = terminal.getTerminalModel()
|
||||
val map = mapOf(
|
||||
"\\r" to ControlCharacters.CR,
|
||||
"\\n" to ControlCharacters.LF,
|
||||
"\\t" to ControlCharacters.TAB,
|
||||
"\\a" to ControlCharacters.BEL,
|
||||
"\\e" to ControlCharacters.ESC,
|
||||
"\\b" to ControlCharacters.BS,
|
||||
)
|
||||
|
||||
if (terminalModel.hasData(DataKey.PtyConnector)) {
|
||||
var text = snippet.snippet
|
||||
for (e in map.entries) {
|
||||
text = text.replace(e.key, e.value.toString())
|
||||
}
|
||||
val ptyConnector = terminalModel.getData(DataKey.PtyConnector)
|
||||
ptyConnector.write(text.toByteArray(ptyConnector.getCharset()))
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/main/kotlin/app/termora/snippet/SnippetBannerPanel.kt
Normal file
50
src/main/kotlin/app/termora/snippet/SnippetBannerPanel.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import java.awt.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SnippetBannerPanel(fontSize: Int = 12) : JComponent() {
|
||||
private val banner = """
|
||||
_____ _ __
|
||||
/ ___/____ (_)___ ____ ___ / /_
|
||||
\__ \/ __ \/ / __ \/ __ \/ _ \/ __/
|
||||
___/ / / / / / /_/ / /_/ / __/ /_
|
||||
/____/_/ /_/_/ .___/ .___/\___/\__/
|
||||
/_/ /_/
|
||||
""".trimIndent().lines()
|
||||
|
||||
init {
|
||||
font = Font("JetBrains Mono", Font.PLAIN, fontSize)
|
||||
preferredSize = Dimension(width, getFontMetrics(font).height * banner.size)
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (g is Graphics2D) {
|
||||
g.setRenderingHints(
|
||||
RenderingHints(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
g.font = font
|
||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||
|
||||
val height = g.fontMetrics.height
|
||||
val descent = g.fontMetrics.descent
|
||||
val offset = width / 2 - g.fontMetrics.stringWidth(banner.maxBy { it.length }) / 2
|
||||
|
||||
for (i in banner.indices) {
|
||||
var x = offset
|
||||
val y = height * (i + 1) - descent
|
||||
val chars = banner[i].toCharArray()
|
||||
for (j in chars.indices) {
|
||||
g.drawChars(chars, j, 1, x, y)
|
||||
x += g.fontMetrics.charWidth(chars[j])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
59
src/main/kotlin/app/termora/snippet/SnippetDialog.kt
Normal file
59
src/main/kotlin/app/termora/snippet/SnippetDialog.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
import kotlin.math.max
|
||||
|
||||
class SnippetDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
init()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val w = properties.getString("SnippetDialog.width", "0").toIntOrNull() ?: 0
|
||||
val h = properties.getString("SnippetDialog.height", "0").toIntOrNull() ?: 0
|
||||
val x = properties.getString("SnippetDialog.x", "-1").toIntOrNull() ?: -1
|
||||
val y = properties.getString("SnippetDialog.y", "-1").toIntOrNull() ?: -1
|
||||
|
||||
size = if (w > 0 && h > 0) {
|
||||
Dimension(w, h)
|
||||
} else {
|
||||
Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
}
|
||||
isModal = true
|
||||
isResizable = true
|
||||
title = I18n.getString("termora.snippet.title")
|
||||
|
||||
if (x != -1 && y != -1) {
|
||||
setLocation(max(x, 0), max(y, 0))
|
||||
} else {
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetDialog.width", width.toString())
|
||||
properties.putString("SnippetDialog.height", height.toString())
|
||||
properties.putString("SnippetDialog.x", x.toString())
|
||||
properties.putString("SnippetDialog.y", y.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return SnippetPanel().apply { Disposer.register(disposable, this) }
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
44
src/main/kotlin/app/termora/snippet/SnippetManager.kt
Normal file
44
src/main/kotlin/app/termora/snippet/SnippetManager.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.Database
|
||||
import app.termora.assertEventDispatchThread
|
||||
|
||||
|
||||
class SnippetManager private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): SnippetManager {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(SnippetManager::class) { SnippetManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val database get() = Database.getDatabase()
|
||||
private var snippets = mutableMapOf<String, Snippet>()
|
||||
|
||||
/**
|
||||
* 修改缓存并存入数据库
|
||||
*/
|
||||
fun addSnippet(snippet: Snippet) {
|
||||
assertEventDispatchThread()
|
||||
database.addSnippet(snippet)
|
||||
if (snippet.deleted) {
|
||||
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id }
|
||||
} else {
|
||||
snippets[snippet.id] = snippet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||
*/
|
||||
fun snippets(): List<Snippet> {
|
||||
if (snippets.isEmpty()) {
|
||||
database.getSnippets().filter { !it.deleted }
|
||||
.forEach { snippets[it.id] = it }
|
||||
}
|
||||
return snippets.values.filter { !it.deleted }
|
||||
.sortedWith(compareBy<Snippet> { if (it.type == SnippetType.Folder) 0 else 1 }.thenBy { it.sort })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
225
src/main/kotlin/app/termora/snippet/SnippetPanel.kt
Normal file
225
src/main/kotlin/app/termora/snippet/SnippetPanel.kt
Normal file
@@ -0,0 +1,225 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.components.FlatTextArea
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.undo.UndoManager
|
||||
|
||||
|
||||
class SnippetPanel : JPanel(BorderLayout()), Disposable {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SnippetPanel::class.java)
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
}
|
||||
|
||||
private val leftPanel = JPanel(BorderLayout())
|
||||
private val cardLayout = CardLayout()
|
||||
private val rightPanel = JPanel(cardLayout)
|
||||
private val snippetTree = SnippetTree()
|
||||
private val editor = SnippetEditor()
|
||||
private val lastNode get() = snippetTree.getLastSelectedPathNode()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val splitPane = JSplitPane()
|
||||
splitPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
leftPanel.add(snippetTree, BorderLayout.CENTER)
|
||||
leftPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
)
|
||||
leftPanel.preferredSize = Dimension(
|
||||
properties.getString("SnippetPanel.LeftPanel.width", "180").toIntOrNull() ?: 180,
|
||||
-1
|
||||
)
|
||||
|
||||
rightPanel.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 1, 0, 0, DynamicColor.BorderColor),
|
||||
BorderFactory.createEmptyBorder(6, 6, 6, 6)
|
||||
)
|
||||
|
||||
val bannerPanel = JPanel(BorderLayout())
|
||||
bannerPanel.add(SnippetBannerPanel(), BorderLayout.CENTER)
|
||||
bannerPanel.border = BorderFactory.createEmptyBorder(32, 0, 0, 0)
|
||||
rightPanel.add(bannerPanel, "Banner")
|
||||
rightPanel.add(editor, "Editor")
|
||||
|
||||
splitPane.leftComponent = leftPanel
|
||||
splitPane.rightComponent = rightPanel
|
||||
add(splitPane, BorderLayout.CENTER)
|
||||
|
||||
cardLayout.show(rightPanel, "Banner")
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
snippetTree.addTreeSelectionListener {
|
||||
val lastNode = this.lastNode
|
||||
if (lastNode == null || lastNode.isFolder) {
|
||||
cardLayout.show(rightPanel, "Banner")
|
||||
} else {
|
||||
cardLayout.show(rightPanel, "Editor")
|
||||
editor.textArea.text = lastNode.data.snippet
|
||||
editor.resetUndo()
|
||||
}
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
if (snippetTree.selectionRows?.isEmpty() == true) {
|
||||
snippetTree.addSelectionRow(0)
|
||||
}
|
||||
snippetTree.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
val expansionState = properties.getString("SnippetPanel.LeftTreePanel.expansionState", StringUtils.EMPTY)
|
||||
if (expansionState.isNotBlank()) {
|
||||
TreeUtils.loadExpansionState(snippetTree, expansionState)
|
||||
}
|
||||
|
||||
val selectionRows = properties.getString("SnippetPanel.LeftTreePanel.selectionRows", StringUtils.EMPTY)
|
||||
if (selectionRows.isNotBlank()) {
|
||||
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetPanel.LeftPanel.width", leftPanel.width.toString())
|
||||
properties.putString("SnippetPanel.LeftPanel.height", leftPanel.height.toString())
|
||||
properties.putString("SnippetPanel.LeftTreePanel.expansionState", TreeUtils.saveExpansionState(snippetTree))
|
||||
properties.putString("SnippetPanel.LeftTreePanel.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
|
||||
}
|
||||
|
||||
|
||||
private inner class SnippetEditor : JPanel(BorderLayout()) {
|
||||
val textArea = FlatTextArea()
|
||||
private var undoManager = UndoManager()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.add(JScrollPane(textArea).apply { border = BorderFactory.createEmptyBorder() }, BorderLayout.CENTER)
|
||||
panel.border = FlatRoundBorder()
|
||||
add(panel, BorderLayout.CENTER)
|
||||
add(createTip(), BorderLayout.SOUTH)
|
||||
|
||||
textArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
textArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
textArea.document.addUndoableEditListener(undoManager)
|
||||
|
||||
textArea.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if ((e.keyCode == KeyEvent.VK_Z || e.keyCode == KeyEvent.VK_Y) && (if (SystemInfo.isMacOS) e.isMetaDown else e.isControlDown)) {
|
||||
try {
|
||||
if (e.keyCode == KeyEvent.VK_Z) {
|
||||
if (undoManager.canUndo()) {
|
||||
undoManager.undo()
|
||||
}
|
||||
} else {
|
||||
if (undoManager.canRedo()) {
|
||||
undoManager.redo()
|
||||
}
|
||||
}
|
||||
} catch (cue: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(cue.message, cue.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||
override fun changedUpdate(e: DocumentEvent) {
|
||||
val lastNode = lastNode ?: return
|
||||
lastNode.data = lastNode.data.copy(snippet = textArea.text, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(lastNode.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun resetUndo() {
|
||||
textArea.document.removeUndoableEditListener(undoManager)
|
||||
undoManager = UndoManager()
|
||||
textArea.document.addUndoableEditListener(undoManager)
|
||||
}
|
||||
|
||||
private fun createTip(): JPanel {
|
||||
val formMargin = "10dlu"
|
||||
val panel = FormBuilder.create().debug(false)
|
||||
.border(
|
||||
BorderFactory.createCompoundBorder(
|
||||
FlatRoundBorder(),
|
||||
BorderFactory.createEmptyBorder(2, 4, 4, 4)
|
||||
)
|
||||
)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, left:pref, $formMargin, left:pref, left:pref, $formMargin, left:pref, left:pref",
|
||||
"pref, $formMargin, pref"
|
||||
)
|
||||
)
|
||||
.add(createTipLabel("\\r - ")).xy(1, 1)
|
||||
.add(createTipLabel("CR")).xy(2, 1)
|
||||
.add(createTipLabel("\\n - ")).xy(4, 1)
|
||||
.add(createTipLabel("LF")).xy(5, 1)
|
||||
.add(createTipLabel("\\t - ")).xy(7, 1)
|
||||
.add(createTipLabel("Tab")).xy(8, 1)
|
||||
|
||||
.add(createTipLabel("\\a - ")).xy(1, 2)
|
||||
.add(createTipLabel("Bell")).xy(2, 2)
|
||||
.add(createTipLabel("\\e - ")).xy(4, 2)
|
||||
.add(createTipLabel("Escape")).xy(5, 2)
|
||||
.add(createTipLabel("\\b - ")).xy(7, 2)
|
||||
.add(createTipLabel("Backspace")).xy(8, 2)
|
||||
.build()
|
||||
|
||||
return JPanel(BorderLayout()).apply {
|
||||
add(panel, BorderLayout.CENTER)
|
||||
border = BorderFactory.createEmptyBorder(4, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTipLabel(text: String): JLabel {
|
||||
val label = JLabel(text)
|
||||
label.foreground = UIManager.getColor("textInactiveText")
|
||||
return label
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
146
src/main/kotlin/app/termora/snippet/SnippetTree.kt
Normal file
146
src/main/kotlin/app/termora/snippet/SnippetTree.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.OptionPane
|
||||
import app.termora.SimpleTree
|
||||
import app.termora.SimpleTreeNode
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.DropMode
|
||||
import javax.swing.JMenu
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class SnippetTree : SimpleTree() {
|
||||
override val model = SnippetTreeModel()
|
||||
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
super.setModel(model)
|
||||
isEditable = true
|
||||
dragEnabled = true
|
||||
dropMode = DropMode.ON_OR_INSERT
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
override fun showContextmenu(evt: MouseEvent) {
|
||||
val lastNode = getLastSelectedPathNode() ?: return
|
||||
val popupMenu = FlatPopupMenu()
|
||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||
val newSnippet = newMenu.add(I18n.getString("termora.snippet"))
|
||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||
popupMenu.addSeparator()
|
||||
val refresh = popupMenu.add(I18n.getString("termora.welcome.contextmenu.refresh"))
|
||||
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
||||
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
||||
popupMenu.addSeparator()
|
||||
|
||||
newFolder.addActionListener {
|
||||
val snippet = Snippet(
|
||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||
type = SnippetType.Folder,
|
||||
parentId = lastNode.data.id
|
||||
)
|
||||
snippetManager.addSnippet(snippet)
|
||||
newFolder(SnippetTreeNode(snippet))
|
||||
}
|
||||
newSnippet.addActionListener {
|
||||
val snippet = Snippet(
|
||||
name = I18n.getString("termora.snippet"),
|
||||
type = SnippetType.Snippet,
|
||||
parentId = lastNode.data.id
|
||||
)
|
||||
snippetManager.addSnippet(snippet)
|
||||
newFile(SnippetTreeNode(snippet))
|
||||
}
|
||||
|
||||
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||
refresh.addActionListener { refreshNode(lastNode) }
|
||||
expandAll.addActionListener {
|
||||
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||
expandPath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
colspanAll.addActionListener {
|
||||
for (node in getSelectionSimpleTreeNodes(true).reversed()) {
|
||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||
}
|
||||
}
|
||||
remove.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val nodes = getSelectionSimpleTreeNodes()
|
||||
if (nodes.isEmpty()) return
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(tree),
|
||||
I18n.getString("termora.keymgr.delete-warning"),
|
||||
I18n.getString("termora.remove"),
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
for (c in nodes) {
|
||||
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
||||
model.removeNodeFromParent(c)
|
||||
// 将所有子孙也删除
|
||||
for (child in c.getAllChildren()) {
|
||||
snippetManager.addSnippet(
|
||||
child.data.copy(
|
||||
deleted = true,
|
||||
updateDate = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
rename.isEnabled = lastNode != model.root
|
||||
remove.isEnabled = rename.isEnabled
|
||||
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
|
||||
newSnippet.isEnabled = newFolder.isEnabled
|
||||
newMenu.isEnabled = newFolder.isEnabled
|
||||
refresh.isEnabled = newFolder.isEnabled
|
||||
|
||||
popupMenu.add(newMenu)
|
||||
|
||||
popupMenu.show(this, evt.x, evt.y)
|
||||
}
|
||||
|
||||
public override fun getLastSelectedPathNode(): SnippetTreeNode? {
|
||||
return super.getLastSelectedPathNode() as? SnippetTreeNode
|
||||
}
|
||||
|
||||
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||
val n = node as? SnippetTreeNode ?: return
|
||||
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(n.data)
|
||||
model.nodeStructureChanged(n)
|
||||
}
|
||||
|
||||
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||
val nNode = node as? SnippetTreeNode ?: return
|
||||
val nParent = parent as? SnippetTreeNode ?: return
|
||||
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(nNode.data)
|
||||
}
|
||||
|
||||
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {
|
||||
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<SnippetTreeNode>()
|
||||
}
|
||||
}
|
||||
65
src/main/kotlin/app/termora/snippet/SnippetTreeDialog.kt
Normal file
65
src/main/kotlin/app/termora/snippet/SnippetTreeDialog.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JScrollPane
|
||||
|
||||
class SnippetTreeDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val snippetTree = SnippetTree()
|
||||
private val properties get() = Database.getDatabase().properties
|
||||
|
||||
var lastNode: SnippetTreeNode? = null
|
||||
|
||||
init {
|
||||
size = Dimension(360, 380)
|
||||
title = I18n.getString("termora.snippet.title")
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
|
||||
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
properties.putString("SnippetTreeDialog.Tree.expansionState", TreeUtils.saveExpansionState(snippetTree))
|
||||
properties.putString("SnippetTreeDialog.Tree.selectionRows", TreeUtils.saveSelectionRows(snippetTree))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
val expansionState = properties.getString("SnippetTreeDialog.Tree.expansionState", StringUtils.EMPTY)
|
||||
if (expansionState.isNotBlank()) {
|
||||
TreeUtils.loadExpansionState(snippetTree, expansionState)
|
||||
}
|
||||
|
||||
val selectionRows = properties.getString("SnippetTreeDialog.Tree.selectionRows", StringUtils.EMPTY)
|
||||
if (selectionRows.isNotBlank()) {
|
||||
TreeUtils.loadSelectionRows(snippetTree, selectionRows)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return JScrollPane(snippetTree).apply { border = BorderFactory.createEmptyBorder(0, 6, 6, 6) }
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
lastNode = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val node = snippetTree.getLastSelectedPathNode() ?: return
|
||||
if (node.isFolder) return
|
||||
lastNode = node
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getSelectedNode(): SnippetTreeNode? {
|
||||
return lastNode
|
||||
}
|
||||
}
|
||||
73
src/main/kotlin/app/termora/snippet/SnippetTreeModel.kt
Normal file
73
src/main/kotlin/app/termora/snippet/SnippetTreeModel.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.SimpleTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
class SnippetTreeModel : SimpleTreeModel<Snippet>(
|
||||
SnippetTreeNode(
|
||||
Snippet(
|
||||
id = "0",
|
||||
name = "全部片段",
|
||||
type = SnippetType.Folder
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
||||
private val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
init {
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun getRoot(): SnippetTreeNode {
|
||||
return super.getRoot() as SnippetTreeNode
|
||||
}
|
||||
|
||||
override fun reload(parent: TreeNode?) {
|
||||
|
||||
if (parent !is SnippetTreeNode) {
|
||||
super.reload(parent)
|
||||
return
|
||||
}
|
||||
|
||||
parent.removeAllChildren()
|
||||
|
||||
val hosts = snippetManager.snippets()
|
||||
val nodes = linkedMapOf<String, SnippetTreeNode>()
|
||||
|
||||
// 遍历 Host 列表,构建树节点
|
||||
for (host in hosts) {
|
||||
val node = SnippetTreeNode(host)
|
||||
nodes[host.id] = node
|
||||
}
|
||||
|
||||
for (host in hosts) {
|
||||
val node = nodes[host.id] ?: continue
|
||||
if (host.parentId.isBlank()) continue
|
||||
val p = nodes[host.parentId] ?: continue
|
||||
p.add(node)
|
||||
}
|
||||
|
||||
for ((_, v) in nodes.entries) {
|
||||
if (parent.data.id == v.data.parentId) {
|
||||
parent.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
super.reload(parent)
|
||||
}
|
||||
|
||||
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
|
||||
super.insertNodeInto(newChild, parent, index)
|
||||
// 重置所有排序
|
||||
if (parent is SnippetTreeNode) {
|
||||
for ((i, c) in parent.children().toList().filterIsInstance<SnippetTreeNode>().withIndex()) {
|
||||
val sort = i.toLong()
|
||||
if (c.data.sort == sort) continue
|
||||
c.data = c.data.copy(sort = sort, updateDate = System.currentTimeMillis())
|
||||
snippetManager.addSnippet(c.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/app/termora/snippet/SnippetTreeNode.kt
Normal file
26
src/main/kotlin/app/termora/snippet/SnippetTreeNode.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package app.termora.snippet
|
||||
|
||||
import app.termora.Icons
|
||||
import app.termora.SimpleTreeNode
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||
import javax.swing.Icon
|
||||
|
||||
class SnippetTreeNode(snippet: Snippet) : SimpleTreeNode<Snippet>(snippet) {
|
||||
|
||||
override val folderCount: Int
|
||||
get() = children().toList().count { if (it is SnippetTreeNode) it.data.type == SnippetType.Folder else false }
|
||||
override val id get() = data.id
|
||||
override val isFolder get() = data.type == SnippetType.Folder
|
||||
|
||||
override fun toString(): String {
|
||||
return data.name
|
||||
}
|
||||
|
||||
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||
return when (data.type) {
|
||||
SnippetType.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
else -> if (selected && hasFocus) Icons.codeSpan.dark else Icons.codeSpan
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,14 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
gistResponse.gists.findLast { it.filename == "Snippets" }?.let {
|
||||
decodeSnippets(it.content, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
|
||||
}
|
||||
@@ -84,6 +92,16 @@ abstract class GitSyncer : SafetySyncer() {
|
||||
gistFiles.add(GistFile("Hosts", hostsContent))
|
||||
}
|
||||
|
||||
|
||||
// Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
val snippetsContent = encodeSnippets(key)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push encryptedSnippets: {}", snippetsContent)
|
||||
}
|
||||
gistFiles.add(GistFile("Snippets", snippetsContent))
|
||||
}
|
||||
|
||||
// KeyPairs
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keysContent = encodeKeys(key)
|
||||
|
||||
@@ -14,7 +14,8 @@ import app.termora.keymgr.KeyManager
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.macro.MacroManager
|
||||
import kotlinx.serialization.encodeToString
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.snippet.SnippetManager
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -32,6 +33,7 @@ abstract class SafetySyncer : Syncer {
|
||||
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||
protected val macroManager get() = MacroManager.getInstance()
|
||||
protected val keymapManager get() = KeymapManager.getInstance()
|
||||
protected val snippetManager get() = SnippetManager.getInstance()
|
||||
|
||||
protected fun decodeHosts(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
@@ -131,6 +133,61 @@ abstract class SafetySyncer : Syncer {
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeSnippets(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
|
||||
val snippets = snippetManager.snippets().associateBy { it.id }
|
||||
|
||||
for (encryptedSnippet in encryptedSnippets) {
|
||||
val oldHost = snippets[encryptedSnippet.id]
|
||||
|
||||
// 如果一样,则无需配置
|
||||
if (oldHost != null) {
|
||||
if (oldHost.updateDate == encryptedSnippet.updateDate) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// aes iv
|
||||
val iv = getIv(encryptedSnippet.id)
|
||||
val snippet = encryptedSnippet.copy(
|
||||
name = encryptedSnippet.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
parentId = encryptedSnippet.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
snippet = encryptedSnippet.snippet.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||
)
|
||||
SwingUtilities.invokeLater { snippetManager.addSnippet(snippet) }
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Decode snippet: ${encryptedSnippet.id} failed. error: {}", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Decode hosts: {}", text)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun encodeSnippets(key: ByteArray): String {
|
||||
val snippets = mutableListOf<Snippet>()
|
||||
for (snippet in snippetManager.snippets()) {
|
||||
// aes iv
|
||||
val iv = ArrayUtils.subarray(snippet.id.padEnd(16, '0').toByteArray(), 0, 16)
|
||||
snippets.add(
|
||||
snippet.copy(
|
||||
name = snippet.name.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
snippet = snippet.snippet.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
parentId = snippet.parentId.aesCBCEncrypt(key, iv).encodeBase64String(),
|
||||
)
|
||||
)
|
||||
}
|
||||
return ohMyJson.encodeToString(snippets)
|
||||
|
||||
}
|
||||
|
||||
protected fun decodeKeys(text: String, config: SyncConfig) {
|
||||
// aes key
|
||||
val key = getKey(config)
|
||||
|
||||
@@ -13,6 +13,7 @@ enum class SyncRange {
|
||||
KeywordHighlights,
|
||||
Macros,
|
||||
Keymap,
|
||||
Snippets,
|
||||
}
|
||||
|
||||
data class SyncConfig(
|
||||
|
||||
@@ -4,7 +4,6 @@ import app.termora.Application.ohMyJson
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.PBKDF2
|
||||
import app.termora.ResponseException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -37,28 +36,45 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||
|
||||
// decode hosts
|
||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||
decodeHosts(it, config)
|
||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||
decodeHosts(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode KeyPairs
|
||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeys(it, config)
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
json["KeyPairs"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeys(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Highlights
|
||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeywordHighlights(it, config)
|
||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||
json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeywordHighlights(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Macros
|
||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||
decodeMacros(it, config)
|
||||
if (config.ranges.contains(SyncRange.Macros)) {
|
||||
json["Macros"]?.jsonPrimitive?.content?.let {
|
||||
decodeMacros(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Keymaps
|
||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeymaps(it, config)
|
||||
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||
json["Keymaps"]?.jsonPrimitive?.content?.let {
|
||||
decodeKeymaps(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
// decode Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
json["Snippets"]?.jsonPrimitive?.content?.let {
|
||||
decodeSnippets(it, config)
|
||||
}
|
||||
}
|
||||
|
||||
return GistResponse(config, emptyList())
|
||||
@@ -77,6 +93,15 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
||||
put("Hosts", hostsContent)
|
||||
}
|
||||
|
||||
// Snippets
|
||||
if (config.ranges.contains(SyncRange.Snippets)) {
|
||||
val snippetsContent = encodeSnippets(key)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Push encryptedSnippets: {}", snippetsContent)
|
||||
}
|
||||
put("Snippets", snippetsContent)
|
||||
}
|
||||
|
||||
// KeyPairs
|
||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||
val keysContent = encodeKeys(key)
|
||||
|
||||
@@ -384,7 +384,7 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
val mode = args.toInt(0)
|
||||
if (mode == 0) {
|
||||
val x = terminal.getCursorModel().getPosition().x
|
||||
terminal.getTabulator().clearTabStop(x)
|
||||
terminal.getTabulator().clearTabStop(x - 1)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Tab Clear (TBC). clearTabStop($x)")
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ class EscapeSequenceProcessor(terminal: Terminal, reader: TerminalReader) : Abst
|
||||
}
|
||||
} else {
|
||||
val x = terminal.getCursorModel().getPosition().x
|
||||
terminal.getTabulator().setTabStop(x)
|
||||
terminal.getTabulator().setTabStop(x - 1)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Horizontal Tab Set (HTS). col: $x")
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F2), encode = "${ControlCharacters.ESC}OQ")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F3), encode = "${ControlCharacters.ESC}OR")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F4), encode = "${ControlCharacters.ESC}OS")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F5), encode = "${ControlCharacters.ESC}[15~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F6), encode = "${ControlCharacters.ESC}[17~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F7), encode = "${ControlCharacters.ESC}[18~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F8), encode = "${ControlCharacters.ESC}[19~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F9), encode = "${ControlCharacters.ESC}[20~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F10), encode = "${ControlCharacters.ESC}[21~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F11), encode = "${ControlCharacters.ESC}[23~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F12), encode = "${ControlCharacters.ESC}[24~");
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F5), encode = "${ControlCharacters.ESC}[15~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F6), encode = "${ControlCharacters.ESC}[17~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F7), encode = "${ControlCharacters.ESC}[18~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F8), encode = "${ControlCharacters.ESC}[19~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F9), encode = "${ControlCharacters.ESC}[20~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F10), encode = "${ControlCharacters.ESC}[21~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F11), encode = "${ControlCharacters.ESC}[23~")
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_F12), encode = "${ControlCharacters.ESC}[24~")
|
||||
|
||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||
@@ -73,7 +73,40 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
||||
}
|
||||
|
||||
override fun encode(event: TerminalKeyEvent): String {
|
||||
return mapping[event] ?: nothing
|
||||
if (mapping.containsKey(event)) {
|
||||
return mapping.getValue(event)
|
||||
}
|
||||
|
||||
var bytes = (mapping[TerminalKeyEvent(event.keyCode, 0)] ?: return nothing).toByteArray()
|
||||
|
||||
if (alwaysSendEsc(event.keyCode) && (event.modifiers and TerminalEvent.ALT_MASK) != 0) {
|
||||
bytes = insertCodeAt(bytes, makeCode(ControlCharacters.ESC.code), 0)
|
||||
return String(bytes)
|
||||
}
|
||||
|
||||
if (alwaysSendEsc(event.keyCode) && (event.modifiers and TerminalEvent.META_MASK) != 0) {
|
||||
bytes = insertCodeAt(bytes, makeCode(ControlCharacters.ESC.code), 0)
|
||||
return String(bytes)
|
||||
}
|
||||
|
||||
if (isCursorKey(event.keyCode) || isFunctionKey(event.keyCode)) {
|
||||
bytes = getCodeWithModifiers(bytes, event.modifiers)
|
||||
return String(bytes)
|
||||
}
|
||||
|
||||
return String(bytes)
|
||||
}
|
||||
|
||||
private fun makeCode(vararg bytesAsInt: Int): ByteArray {
|
||||
val bytes = ByteArray(bytesAsInt.size)
|
||||
for ((i, byteAsInt) in bytesAsInt.withIndex()) {
|
||||
bytes[i] = byteAsInt.toByte()
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun alwaysSendEsc(key: Int): Boolean {
|
||||
return isCursorKey(key) || key == '\b'.code
|
||||
}
|
||||
|
||||
override fun getTerminal(): Terminal {
|
||||
@@ -84,6 +117,91 @@ open class KeyEncoderImpl(private val terminal: Terminal) : KeyEncoder, DataList
|
||||
mapping[event] = encode
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refer to section PC-Style Function Keys in http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
*/
|
||||
private fun getCodeWithModifiers(bytes: ByteArray, modifiers: Int): ByteArray {
|
||||
val code = modifiersToCode(modifiers)
|
||||
|
||||
if (code > 0 && bytes.size > 2) {
|
||||
// SS3 needs to become CSI.
|
||||
if (bytes[0].toInt() == ControlCharacters.ESC.code && bytes[1] == 'O'.code.toByte()) {
|
||||
bytes[1] = '['.code.toByte()
|
||||
}
|
||||
// If the control sequence has no parameters, it needs a default parameter.
|
||||
// Either way it also needs a semicolon separator.
|
||||
val prefix = if (bytes.size == 3) "1;" else ";"
|
||||
return insertCodeAt(
|
||||
bytes,
|
||||
(prefix + code).toByteArray(),
|
||||
bytes.size - 1
|
||||
)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun insertCodeAt(bytes: ByteArray, code: ByteArray, at: Int): ByteArray {
|
||||
val res = ByteArray(bytes.size + code.size)
|
||||
System.arraycopy(bytes, 0, res, 0, bytes.size)
|
||||
System.arraycopy(bytes, at, res, at + code.size, bytes.size - at)
|
||||
System.arraycopy(code, 0, res, at, code.size)
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Code Modifiers
|
||||
* ------+--------------------------
|
||||
* 2 | Shift
|
||||
* 3 | Alt
|
||||
* 4 | Shift + Alt
|
||||
* 5 | Control
|
||||
* 6 | Shift + Control
|
||||
* 7 | Alt + Control
|
||||
* 8 | Shift + Alt + Control
|
||||
* 9 | Meta
|
||||
* 10 | Meta + Shift
|
||||
* 11 | Meta + Alt
|
||||
* 12 | Meta + Alt + Shift
|
||||
* 13 | Meta + Ctrl
|
||||
* 14 | Meta + Ctrl + Shift
|
||||
* 15 | Meta + Ctrl + Alt
|
||||
* 16 | Meta + Ctrl + Alt + Shift
|
||||
* ------+--------------------------
|
||||
* @param modifiers
|
||||
* @return
|
||||
*/
|
||||
private fun modifiersToCode(modifiers: Int): Int {
|
||||
var code = 0
|
||||
if ((modifiers and TerminalEvent.SHIFT_MASK) != 0) {
|
||||
code = code or 1
|
||||
}
|
||||
if ((modifiers and TerminalEvent.ALT_MASK) != 0) {
|
||||
code = code or 2
|
||||
}
|
||||
if ((modifiers and TerminalEvent.CTRL_MASK) != 0) {
|
||||
code = code or 4
|
||||
}
|
||||
if ((modifiers and TerminalEvent.META_MASK) != 0) {
|
||||
code = code or 8
|
||||
}
|
||||
return if (code != 0) code + 1 else 0
|
||||
}
|
||||
|
||||
private fun isCursorKey(key: Int): Boolean {
|
||||
return key == KeyEvent.VK_DOWN || key == KeyEvent.VK_UP
|
||||
|| key == KeyEvent.VK_LEFT || key == KeyEvent.VK_RIGHT
|
||||
|| key == KeyEvent.VK_HOME || key == KeyEvent.VK_END
|
||||
}
|
||||
|
||||
private fun isFunctionKey(key: Int): Boolean {
|
||||
return key >= KeyEvent.VK_F1 && key <= KeyEvent.VK_F12
|
||||
|| key == KeyEvent.VK_INSERT || key == KeyEvent.VK_DELETE
|
||||
|| key == KeyEvent.VK_PAGE_UP || key == KeyEvent.VK_PAGE_DOWN
|
||||
}
|
||||
|
||||
fun arrowKeysApplicationSequences() {
|
||||
// Up
|
||||
putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_UP), encode = "${ControlCharacters.ESC}OA")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.termora.terminal
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.math.min
|
||||
import kotlin.math.max
|
||||
|
||||
open class VisualTerminal : Terminal {
|
||||
|
||||
@@ -119,6 +119,9 @@ open class VisualTerminal : Terminal {
|
||||
|
||||
private class MyProcessor(private val terminal: Terminal, reader: TerminalReader) {
|
||||
private var state: ProcessorState = TerminalState.READY
|
||||
private val document get() = terminal.getDocument()
|
||||
private val cursorModel get() = terminal.getCursorModel()
|
||||
private val terminalModel get() = terminal.getTerminalModel()
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MyProcessor::class.java)
|
||||
@@ -135,7 +138,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
||||
|
||||
fun process(ch: Char) {
|
||||
if (log.isTraceEnabled) {
|
||||
val position = terminal.getCursorModel().getPosition()
|
||||
val position = cursorModel.getPosition()
|
||||
log.trace("process [${printChar(ch)}] , state: $state , position: $position")
|
||||
}
|
||||
|
||||
@@ -155,16 +158,29 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
||||
}
|
||||
|
||||
ControlCharacters.CR -> {
|
||||
terminal.getCursorModel().move(CursorMove.RowHome)
|
||||
cursorModel.move(CursorMove.RowHome)
|
||||
TerminalState.READY
|
||||
}
|
||||
|
||||
ControlCharacters.TAB -> {
|
||||
val position = terminal.getCursorModel().getPosition()
|
||||
val position = cursorModel.getPosition()
|
||||
// Next tab + 1,如果当前 x = 11,那么下一个就是 16,因为在 TerminalLineBuffer#writeTerminalLineChar 的时候会 - 1 会导致错乱一位
|
||||
var nextTab = terminal.getTabulator().nextTab(position.x) + 1
|
||||
nextTab = min(terminal.getTerminalModel().getCols(), nextTab)
|
||||
terminal.getCursorModel().move(row = position.y, col = nextTab)
|
||||
val nextTab = terminal.getTabulator().nextTab(position.x - 1) + 1
|
||||
val length = if (terminalModel.isAlternateScreenBuffer()) {
|
||||
document.getCurrentTerminalLineBuffer()
|
||||
.getLineAt(position.y - 1).getText().length
|
||||
} else {
|
||||
document.getCurrentTerminalLineBuffer()
|
||||
.getScreenLineAt(position.y - 1)
|
||||
.getText().length
|
||||
}
|
||||
|
||||
val x = max(position.x - 1, length)
|
||||
if (x < nextTab) {
|
||||
cursorModel.move(row = position.y, col = (position.x - 1) + (nextTab - x))
|
||||
} else {
|
||||
cursorModel.move(row = position.y, col = nextTab)
|
||||
}
|
||||
TerminalState.READY
|
||||
}
|
||||
|
||||
@@ -176,12 +192,12 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
||||
}
|
||||
|
||||
ControlCharacters.BS -> {
|
||||
terminal.getCursorModel().move(CursorMove.Left)
|
||||
cursorModel.move(CursorMove.Left)
|
||||
TerminalState.READY
|
||||
}
|
||||
|
||||
ControlCharacters.SI -> {
|
||||
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G0)
|
||||
terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G0)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Use Graphic.G0")
|
||||
}
|
||||
@@ -189,7 +205,7 @@ private class MyProcessor(private val terminal: Terminal, reader: TerminalReader
|
||||
}
|
||||
|
||||
ControlCharacters.SO -> {
|
||||
terminal.getTerminalModel().getData(DataKey.GraphicCharacterSet).use(Graphic.G1)
|
||||
terminalModel.getData(DataKey.GraphicCharacterSet).use(Graphic.G1)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Use Graphic.G1")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.snippet.SnippetAction
|
||||
import app.termora.snippet.SnippetTreeDialog
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||
@@ -59,7 +61,6 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
|
||||
init {
|
||||
border = FlatRoundBorder()
|
||||
isOpaque = false
|
||||
isFocusable = false
|
||||
isFloatable = false
|
||||
isVisible = false
|
||||
@@ -96,7 +97,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (isVisible == true) {
|
||||
if (isVisible) {
|
||||
isVisible = false
|
||||
firePropertyChange("visible", true, false)
|
||||
}
|
||||
@@ -109,6 +110,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Snippet
|
||||
add(initSnippetActionButton())
|
||||
|
||||
// Nvidia 显卡信息
|
||||
add(initNvidiaSMIActionButton())
|
||||
|
||||
@@ -147,6 +151,24 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initSnippetActionButton(): JButton {
|
||||
val btn = JButton(Icons.codeSpan)
|
||||
btn.toolTipText = I18n.getString("termora.snippet.title")
|
||||
btn.addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||
val terminal = tab.getData(DataProviders.Terminal) ?: return
|
||||
val dialog = SnippetTreeDialog(evt.window)
|
||||
dialog.setLocationRelativeTo(btn)
|
||||
dialog.setLocation(dialog.x, btn.locationOnScreen.y + height + 2)
|
||||
dialog.isVisible = true
|
||||
val node = dialog.getSelectedNode() ?: return
|
||||
SnippetAction.getInstance().runSnippet(node.data, terminal)
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initNvidiaSMIActionButton(): JButton {
|
||||
val btn = JButton(Icons.nvidia)
|
||||
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
@@ -195,6 +217,7 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
|
||||
private fun initCloseActionButton(): JButton {
|
||||
val btn = JButton(Icons.closeSmall)
|
||||
btn.toolTipText = I18n.getString("termora.floating-toolbar.close-in-current-tab")
|
||||
btn.pressedIcon = Icons.closeSmallHovered
|
||||
btn.rolloverIcon = Icons.closeSmallHovered
|
||||
btn.addActionListener {
|
||||
|
||||
@@ -156,6 +156,18 @@ class FileTransportPanel(
|
||||
deleteAll.isEnabled = transportManager.getTransports().isNotEmpty()
|
||||
}
|
||||
|
||||
popupMenu.addSeparator()
|
||||
|
||||
popupMenu.add(I18n.getString("termora.transport.jobs.table.status")).addActionListener {
|
||||
val last = transports.last()
|
||||
OptionPane.showMessageDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
if (last.state == TransportState.Failed && last.stateText.isNotBlank()) last.stateText
|
||||
else tableModel.formatStatus(last.state),
|
||||
messageType = if (last.state == TransportState.Failed) JOptionPane.ERROR_MESSAGE else JOptionPane.INFORMATION_MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
popupMenu.show(table, event.x, event.y)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class FileTransportTableModel(transportManager: TransportManager) : DefaultTable
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatStatus(state: TransportState): String {
|
||||
fun formatStatus(state: TransportState): String {
|
||||
return when (state) {
|
||||
TransportState.Transporting -> I18n.getString("termora.transport.sftp.status.transporting")
|
||||
TransportState.Waiting -> I18n.getString("termora.transport.sftp.status.waiting")
|
||||
|
||||
@@ -3,6 +3,8 @@ package app.termora.transport
|
||||
import app.termora.Disposable
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.ObjectUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.net.io.CopyStreamEvent
|
||||
import org.apache.commons.net.io.CopyStreamListener
|
||||
import org.apache.commons.net.io.Util
|
||||
@@ -47,6 +49,7 @@ abstract class Transport(
|
||||
field = value
|
||||
listeners.forEach { it.onTransportChanged(this) }
|
||||
}
|
||||
var stateText: String = StringUtils.EMPTY
|
||||
|
||||
// 0 - 1
|
||||
var progress = 0.0
|
||||
@@ -186,6 +189,7 @@ class FileTransport(
|
||||
log.error(e.message, e)
|
||||
}
|
||||
state = TransportState.Failed
|
||||
stateText = ExceptionUtils.getRootCauseMessage(e)
|
||||
} finally {
|
||||
counter.clear()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ termora.quit-confirm=Quit {0}?
|
||||
# update
|
||||
termora.update.title=New version
|
||||
termora.update.update=Update
|
||||
termora.update.ignore=Ignore this version
|
||||
termora.update.ignore=Remind me next time
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Data is encrypted
|
||||
@@ -251,6 +251,10 @@ termora.macro.playback=Playback
|
||||
termora.macro.manager=Manage Macros
|
||||
termora.macro.run=Run
|
||||
|
||||
# Snippets
|
||||
termora.snippet=Snippet
|
||||
termora.snippet.title=Snippets
|
||||
|
||||
# Tools
|
||||
termora.tools.multiple=Send commands to multiple sessions
|
||||
|
||||
@@ -364,6 +368,7 @@ termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||
|
||||
|
||||
termora.floating-toolbar.not-supported=This action is not supported
|
||||
termora.floating-toolbar.close-in-current-tab=Close in current tab
|
||||
|
||||
|
||||
# zmodem
|
||||
|
||||
@@ -15,7 +15,7 @@ termora.quit-confirm=你要退出 {0} 吗?
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
termora.update.ignore=忽略这个版本
|
||||
termora.update.ignore=下次提醒我
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=数据已加密
|
||||
@@ -245,6 +245,11 @@ termora.macro.manager=管理宏
|
||||
termora.macro.run=运行
|
||||
|
||||
|
||||
# Snippets
|
||||
termora.snippet=片段
|
||||
termora.snippet.title=代码片段
|
||||
|
||||
|
||||
|
||||
# Transport
|
||||
termora.transport.local=本机
|
||||
@@ -346,6 +351,7 @@ termora.visual-window.system-information.filesystem=文件系统
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
|
||||
termora.floating-toolbar.not-supported=不允许此操作
|
||||
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭
|
||||
|
||||
|
||||
# zmodem
|
||||
|
||||
@@ -14,7 +14,7 @@ termora.quit-confirm=你要退出 {0} 嗎?
|
||||
# update
|
||||
termora.update.title=新版本
|
||||
termora.update.update=更新
|
||||
termora.update.ignore=忽略這個版本
|
||||
termora.update.ignore=下次提醒我
|
||||
|
||||
|
||||
|
||||
@@ -239,6 +239,13 @@ termora.macro.playback=回放
|
||||
termora.macro.manager=管理宏
|
||||
termora.macro.run=運行
|
||||
|
||||
|
||||
# Snippets
|
||||
termora.snippet=片段
|
||||
termora.snippet.title=程式碼片段
|
||||
|
||||
|
||||
|
||||
# Transport
|
||||
termora.transport.local=本機
|
||||
termora.transport.parent-folder=父資料夾
|
||||
@@ -325,6 +332,7 @@ termora.visual-window.system-information.filesystem=檔案系統
|
||||
termora.visual-window.system-information.used-total=使用 / 大小
|
||||
|
||||
termora.floating-toolbar.not-supported=不允許此操作
|
||||
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
|
||||
|
||||
# zmodem
|
||||
termora.addons.zmodem.skip=跳過
|
||||
4
src/main/resources/icons/anyType.svg
Normal file
4
src/main/resources/icons/anyType.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 1H7.91421C7.51639 1 7.13486 1.15803 6.85355 1.43934L3.43934 4.85355C3.15804 5.13486 3 5.51639 3 5.91421V13C3 14.1046 3.89543 15 5 15H11C12.1046 15 13 14.1046 13 13V3C13 1.89543 12.1046 1 11 1ZM4 13C4 13.5523 4.44772 14 5 14H11C11.5523 14 12 13.5523 12 13V3C12 2.44772 11.5523 2 11 2H8V4.5C8 5.32843 7.32843 6 6.5 6H4V13ZM4.70711 5L7 2.70711V4.5C7 4.77614 6.77614 5 6.5 5H4.70711Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
4
src/main/resources/icons/anyType_dark.svg
Normal file
4
src/main/resources/icons/anyType_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 1H7.91421C7.51639 1 7.13486 1.15803 6.85355 1.43934L3.43934 4.85355C3.15804 5.13486 3 5.51639 3 5.91421V13C3 14.1046 3.89543 15 5 15H11C12.1046 15 13 14.1046 13 13V3C13 1.89543 12.1046 1 11 1ZM4 13C4 13.5523 4.44772 14 5 14H11C11.5523 14 12 13.5523 12 13V3C12 2.44772 11.5523 2 11 2H8V4.5C8 5.32843 7.32843 6 6.5 6H4V13ZM4.70711 5L7 2.70711V4.5C7 4.77614 6.77614 5 6.5 5H4.70711Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
4
src/main/resources/icons/codeSpan.svg
Normal file
4
src/main/resources/icons/codeSpan.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.85355 4.35355C6.04882 4.15829 6.04882 3.84171 5.85355 3.64645C5.65829 3.45118 5.34171 3.45118 5.14645 3.64645L1.14645 7.64645C0.951184 7.84171 0.951184 8.15829 1.14645 8.35355L5.14645 12.3536C5.34171 12.5488 5.65829 12.5488 5.85355 12.3536C6.04882 12.1583 6.04882 11.8417 5.85355 11.6464L2.20711 8L5.85355 4.35355Z" fill="#6C707E"/>
|
||||
<path d="M10.8536 3.64645C10.6583 3.45118 10.3417 3.45118 10.1464 3.64645C9.95118 3.84171 9.95118 4.15829 10.1464 4.35355L13.7929 8L10.1464 11.6464C9.95118 11.8417 9.95118 12.1583 10.1464 12.3536C10.3417 12.5488 10.6583 12.5488 10.8536 12.3536L14.8536 8.35355C15.0488 8.15829 15.0488 7.84171 14.8536 7.64645L10.8536 3.64645Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
4
src/main/resources/icons/codeSpan_dark.svg
Normal file
4
src/main/resources/icons/codeSpan_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.85355 4.35355C6.04882 4.15829 6.04882 3.84171 5.85355 3.64645C5.65829 3.45118 5.34171 3.45118 5.14645 3.64645L1.14645 7.64645C0.951184 7.84171 0.951184 8.15829 1.14645 8.35355L5.14645 12.3536C5.34171 12.5488 5.65829 12.5488 5.85355 12.3536C6.04882 12.1583 6.04882 11.8417 5.85355 11.6464L2.20711 8L5.85355 4.35355Z" fill="#CED0D6"/>
|
||||
<path d="M10.8536 3.64645C10.6583 3.45118 10.3417 3.45118 10.1464 3.64645C9.95118 3.84171 9.95118 4.15829 10.1464 4.35355L13.7929 8L10.1464 11.6464C9.95118 11.8417 9.95118 12.1583 10.1464 12.3536C10.3417 12.5488 10.6583 12.5488 10.8536 12.3536L14.8536 8.35355C15.0488 8.15829 15.0488 7.84171 14.8536 7.64645L10.8536 3.64645Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
5
src/main/resources/icons/toolWindowJsonPath.svg
Normal file
5
src/main/resources/icons/toolWindowJsonPath.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.98755 9.77751C3.99755 9.39251 3.87005 9.08501 3.60505 8.85501C3.34005 8.62501 2.98255 8.51001 2.53255 8.51001H2V7.43001H2.53255C2.98255 7.43001 3.34005 7.31501 3.60505 7.08501C3.87005 6.85501 3.99755 6.54751 3.98755 6.16251L3.95755 4.58C3.94755 4.06 4.05505 3.605 4.28005 3.215C4.51005 2.825 4.83755 2.525 5.26255 2.315C5.68755 2.105 6.18755 2 6.76255 2H7.5V3.005H6.77005C6.25005 3.005 5.83755 3.15 5.53255 3.44C5.23255 3.725 5.08505 4.1125 5.09005 4.6025L5.12005 6.17001C5.13005 6.68001 4.99255 7.09751 4.70755 7.42251C4.42255 7.74251 4.03755 7.92001 3.55255 7.95501C4.03755 8.04501 4.42255 8.25501 4.70755 8.58501C4.99255 8.91001 5.13005 9.30501 5.12005 9.77001L5.09005 11.5475C5.08005 11.9875 5.21255 12.3375 5.48755 12.5975C5.76755 12.8625 6.14505 12.995 6.62005 12.995H7.5V14H6.61255C6.07755 14 5.60755 13.9 5.20255 13.7C4.80255 13.505 4.49255 13.2225 4.27255 12.8525C4.05755 12.4875 3.95255 12.06 3.95755 11.57L3.98755 9.77751Z" fill="#6C707E"/>
|
||||
<path d="M12.0145 9.77751C12.0045 9.39251 12.132 9.08501 12.397 8.85501C12.662 8.62501 13.0195 8.51001 13.4695 8.51001H14V7.43001H13.4695C13.0195 7.43001 12.662 7.31501 12.397 7.08501C12.132 6.85501 12.0045 6.54751 12.0145 6.16251L12.0445 4.58C12.0545 4.06 11.9445 3.605 11.7145 3.215C11.4895 2.825 11.1645 2.525 10.7395 2.315C10.3145 2.105 9.81455 2 9.23955 2H8.5V3.005H9.23205C9.75205 3.005 10.162 3.15 10.462 3.44C10.767 3.725 10.917 4.1125 10.912 4.6025L10.882 6.17001C10.872 6.68001 11.0095 7.09751 11.2945 7.42251C11.5795 7.74251 11.9645 7.92001 12.4495 7.95501C11.9645 8.04501 11.5795 8.25501 11.2945 8.58501C11.0095 8.91001 10.872 9.30501 10.882 9.77001L10.912 11.5475C10.922 11.9875 10.787 12.3375 10.507 12.5975C10.232 12.8625 9.85705 12.995 9.38205 12.995H8.5V14H9.38955C9.92455 14 10.392 13.9 10.792 13.7C11.197 13.505 11.507 13.2225 11.722 12.8525C11.942 12.4875 12.0495 12.06 12.0445 11.57L12.0145 9.77751Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
5
src/main/resources/icons/toolWindowJsonPath_dark.svg
Normal file
5
src/main/resources/icons/toolWindowJsonPath_dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.98755 9.77751C3.99755 9.39251 3.87005 9.08501 3.60505 8.85501C3.34005 8.62501 2.98255 8.51001 2.53255 8.51001H2V7.43001H2.53255C2.98255 7.43001 3.34005 7.31501 3.60505 7.08501C3.87005 6.85501 3.99755 6.54751 3.98755 6.16251L3.95755 4.58C3.94755 4.06 4.05505 3.605 4.28005 3.215C4.51005 2.825 4.83755 2.525 5.26255 2.315C5.68755 2.105 6.18755 2 6.76255 2H7.5V3.005H6.77005C6.25005 3.005 5.83755 3.15 5.53255 3.44C5.23255 3.725 5.08505 4.1125 5.09005 4.6025L5.12005 6.17001C5.13005 6.68001 4.99255 7.09751 4.70755 7.42251C4.42255 7.74251 4.03755 7.92001 3.55255 7.95501C4.03755 8.04501 4.42255 8.25501 4.70755 8.58501C4.99255 8.91001 5.13005 9.30501 5.12005 9.77001L5.09005 11.5475C5.08005 11.9875 5.21255 12.3375 5.48755 12.5975C5.76755 12.8625 6.14505 12.995 6.62005 12.995H7.5V14H6.61255C6.07755 14 5.60755 13.9 5.20255 13.7C4.80255 13.505 4.49255 13.2225 4.27255 12.8525C4.05755 12.4875 3.95255 12.06 3.95755 11.57L3.98755 9.77751Z" fill="#CED0D6"/>
|
||||
<path d="M12.0145 9.77751C12.0045 9.39251 12.132 9.08501 12.397 8.85501C12.662 8.62501 13.0195 8.51001 13.4695 8.51001H14V7.43001H13.4695C13.0195 7.43001 12.662 7.31501 12.397 7.08501C12.132 6.85501 12.0045 6.54751 12.0145 6.16251L12.0445 4.58C12.0545 4.06 11.9445 3.605 11.7145 3.215C11.4895 2.825 11.1645 2.525 10.7395 2.315C10.3145 2.105 9.81455 2 9.23955 2H8.5V3.005H9.23205C9.75205 3.005 10.162 3.15 10.462 3.44C10.767 3.725 10.917 4.1125 10.912 4.6025L10.882 6.17001C10.872 6.68001 11.0095 7.09751 11.2945 7.42251C11.5795 7.74251 11.9645 7.92001 12.4495 7.95501C11.9645 8.04501 11.5795 8.25501 11.2945 8.58501C11.0095 8.91001 10.872 9.30501 10.882 9.77001L10.912 11.5475C10.922 11.9875 10.787 12.3375 10.507 12.5975C10.232 12.8625 9.85705 12.995 9.38205 12.995H8.5V14H9.38955C9.92455 14 10.392 13.9 10.792 13.7C11.197 13.505 11.507 13.2225 11.722 12.8525C11.942 12.4875 12.0495 12.06 12.0445 11.57L12.0145 9.77751Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user