From 287f6973f0bf760d9534c73c21d40aca7648b63b Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 29 Jun 2025 12:08:24 +0800 Subject: [PATCH] chore: improve editor --- plugins/editor/build.gradle.kts | 2 +- .../termora/plugins/editor/EditorDialog.kt | 16 +++- .../app/termora/plugins/editor/EditorPanel.kt | 86 ++++++++++++++++++- src/main/kotlin/app/termora/Icons.kt | 4 + .../app/termora/transfer/TransportPanel.kt | 18 +++- src/main/resources/icons/reformatCode.svg | 8 ++ .../resources/icons/reformatCode_dark.svg | 8 ++ src/main/resources/icons/scrollDown.svg | 5 ++ src/main/resources/icons/scrollDown_dark.svg | 5 ++ src/main/resources/icons/scrollUp.svg | 7 ++ src/main/resources/icons/scrollUp_dark.svg | 7 ++ src/main/resources/icons/softWrap.svg | 4 + src/main/resources/icons/softWrap_dark.svg | 4 + 13 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/icons/reformatCode.svg create mode 100644 src/main/resources/icons/reformatCode_dark.svg create mode 100644 src/main/resources/icons/scrollDown.svg create mode 100644 src/main/resources/icons/scrollDown_dark.svg create mode 100644 src/main/resources/icons/scrollUp.svg create mode 100644 src/main/resources/icons/scrollUp_dark.svg create mode 100644 src/main/resources/icons/softWrap.svg create mode 100644 src/main/resources/icons/softWrap_dark.svg diff --git a/plugins/editor/build.gradle.kts b/plugins/editor/build.gradle.kts index 4cb2e65..2126a4c 100644 --- a/plugins/editor/build.gradle.kts +++ b/plugins/editor/build.gradle.kts @@ -4,7 +4,7 @@ plugins { -project.version = "0.0.4" +project.version = "0.0.5" dependencies { diff --git a/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorDialog.kt b/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorDialog.kt index 51c30f5..9dc4ffd 100644 --- a/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorDialog.kt +++ b/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorDialog.kt @@ -17,15 +17,13 @@ import kotlin.io.path.absolutePathString import kotlin.io.path.name -class EditorDialog(file: Path, owner: Window, myDisposable: Disposable) : DialogWrapper(null) { +class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) { private val filename = file.name private val filepath = File(file.absolutePathString()) private val editorPanel = EditorPanel(this, filepath) init { - Disposer.register(disposable, myDisposable) - size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")) isModal = false controlsVisible = true @@ -49,6 +47,18 @@ class EditorDialog(file: Path, owner: Window, myDisposable: Disposable) : Dialog doCancelAction() } }) + + Disposer.register(myDisposable, object : Disposable { + override fun dispose() { + doCancelAction() + } + }) + + Disposer.register(disposable, object : Disposable { + override fun dispose() { + Disposer.dispose(myDisposable) + } + }) } override fun doCancelAction() { diff --git a/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorPanel.kt b/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorPanel.kt index bd99e58..1bbdd37 100644 --- a/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorPanel.kt +++ b/plugins/editor/src/main/kotlin/app/termora/plugins/editor/EditorPanel.kt @@ -2,18 +2,24 @@ package app.termora.plugins.editor import app.termora.DocumentAdaptor import app.termora.DynamicColor +import app.termora.EnableManager import app.termora.Icons import app.termora.database.DatabaseManager import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.extras.components.FlatTextField import com.formdev.flatlaf.extras.components.FlatToolBar +import kotlinx.serialization.json.Json import org.apache.commons.io.FilenameUtils +import org.dom4j.io.OutputFormat +import org.dom4j.io.SAXReader +import org.dom4j.io.XMLWriter import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea import org.fife.ui.rsyntaxtextarea.SyntaxConstants import org.fife.ui.rsyntaxtextarea.Theme import org.fife.ui.rtextarea.RTextScrollPane import org.fife.ui.rtextarea.SearchContext import org.fife.ui.rtextarea.SearchEngine +import org.slf4j.LoggerFactory import java.awt.BorderLayout import java.awt.Insets import java.awt.event.ActionEvent @@ -21,22 +27,40 @@ import java.awt.event.KeyEvent import java.awt.event.WindowAdapter import java.awt.event.WindowEvent import java.io.File +import java.io.StringReader +import java.io.StringWriter import javax.swing.* +import javax.swing.SwingConstants.VERTICAL import javax.swing.event.DocumentEvent import kotlin.math.max class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) { + + companion object { + private val log = LoggerFactory.getLogger(EditorPanel::class.java) + } + private var text = file.readText(Charsets.UTF_8) private val layeredPane = LayeredPane() private val textArea = RSyntaxTextArea() private val scrollPane = RTextScrollPane(textArea) private val findPanel = FlatToolBar().apply { isFloatable = false } + private val toolbar = FlatToolBar().apply { isFloatable = false } private val searchTextField = FlatTextField() private val closeFindPanelBtn = JButton(Icons.close) private val nextBtn = JButton(Icons.down) private val prevBtn = JButton(Icons.up) private val context = SearchContext() + private val softWrapBtn = JToggleButton(Icons.softWrap) + private val scrollUpBtn = JButton(Icons.scrollUp) + private val scrollEndBtn = JButton(Icons.scrollDown) + private val prettyBtn = JButton(Icons.reformatCode) + + private val enableManager get() = EnableManager.getInstance() + private val prettyJson = Json { + prettyPrint = true + } init { initView() @@ -48,6 +72,7 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat()) textArea.text = text textArea.antiAliasingEnabled = true + softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false) val theme = if (FlatLaf.isLafDark()) Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml")) @@ -89,9 +114,13 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( else -> SyntaxConstants.SYNTAX_STYLE_NONE } + // 只有 JSON 才可以格式化 + prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON || + textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML + textArea.discardAllEdits() - scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) + scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor) findPanel.isVisible = false findPanel.isOpaque = true @@ -110,8 +139,19 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( BorderFactory.createEmptyBorder(2, 2, 2, 2) ) + toolbar.orientation = VERTICAL + toolbar.add(scrollUpBtn) + toolbar.add(prettyBtn) + toolbar.add(softWrapBtn) + toolbar.add(scrollEndBtn) + + val viewPanel = JPanel(BorderLayout()) + viewPanel.add(scrollPane, BorderLayout.CENTER) + viewPanel.add(toolbar, BorderLayout.EAST) + viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) + layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any) - layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) + layeredPane.add(viewPanel, JLayeredPane.DEFAULT_LAYER as Any) add(layeredPane, BorderLayout.CENTER) } @@ -126,6 +166,13 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( } }) + softWrapBtn.addActionListener { + enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected) + textArea.lineWrap = softWrapBtn.isSelected + } + + scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 } + scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum } textArea.inputMap.put( KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx), @@ -135,6 +182,10 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx), "Find" ) + textArea.inputMap.put( + KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK), + "Format" + ) searchTextField.inputMap.put( KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), @@ -159,6 +210,33 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( } }) + textArea.actionMap.put("Format", object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) { + runCatching { + val json = prettyJson.parseToJsonElement(textArea.text) + textArea.text = prettyJson.encodeToString(json) + }.onFailure { + if (log.isErrorEnabled) { + log.error(it.message, it) + } + } + } else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) { + runCatching { + val document = SAXReader().read(StringReader(textArea.text)) + val sw = StringWriter() + val writer = XMLWriter(sw, OutputFormat.createPrettyPrint()) + writer.write(document) + textArea.text = sw.toString() + }.onFailure { + if (log.isErrorEnabled) { + log.error(it.message, it) + } + } + } + } + }) + textArea.actionMap.put("Find", object : AbstractAction("Find") { override fun actionPerformed(e: ActionEvent) { findPanel.isVisible = true @@ -185,6 +263,10 @@ class EditorPanel(private val window: JDialog, private val file: File) : JPanel( searchTextField.addActionListener { nextBtn.doClick(0) } + + + prettyBtn.addActionListener(searchTextField.actionMap.get("Format")) + prevBtn.addActionListener { search(false) } nextBtn.addActionListener { search(true) } } diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index a441861..3fcbd49 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -33,6 +33,10 @@ object Icons { val empty by lazy { DynamicIcon("icons/empty.svg") } val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") } val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") } + val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") } + val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") } + val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") } + val scrollDown by lazy { DynamicIcon("icons/scrollDown.svg", "icons/scrollDown_dark.svg") } val locate by lazy { DynamicIcon("icons/locate.svg", "icons/locate_dark.svg") } val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") } val text by lazy { DynamicIcon("icons/text.svg", "icons/text_dark.svg") } diff --git a/src/main/kotlin/app/termora/transfer/TransportPanel.kt b/src/main/kotlin/app/termora/transfer/TransportPanel.kt index 35316da..8e3dfef 100644 --- a/src/main/kotlin/app/termora/transfer/TransportPanel.kt +++ b/src/main/kotlin/app/termora/transfer/TransportPanel.kt @@ -882,6 +882,11 @@ class TransportPanel( private inner class EditTransferListener : TransferListener, Disposable { private val transferIds = mutableSetOf() private val sftp get() = DatabaseManager.getInstance().sftp + private val parentDisposable = Disposer.newDisposable() + + init { + Disposer.register(this, parentDisposable) + } override fun onTransferChanged( transfer: Transfer, @@ -915,13 +920,22 @@ class TransportPanel( } } + val disposed = AtomicBoolean(false) Disposer.register(disposable, object : Disposable { override fun dispose() { - job.cancel() + disposed.compareAndSet(false, true) + } + }) + + Disposer.register(parentDisposable, object : Disposable { + override fun dispose() { + job.cancel() + if (disposed.compareAndSet(false, true)) { + Disposer.dispose(disposable) + } } }) - Disposer.register(this, disposable) } private fun startEditor(localPath: Path): Disposable { diff --git a/src/main/resources/icons/reformatCode.svg b/src/main/resources/icons/reformatCode.svg new file mode 100644 index 0000000..ef85a66 --- /dev/null +++ b/src/main/resources/icons/reformatCode.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/icons/reformatCode_dark.svg b/src/main/resources/icons/reformatCode_dark.svg new file mode 100644 index 0000000..20872d3 --- /dev/null +++ b/src/main/resources/icons/reformatCode_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/icons/scrollDown.svg b/src/main/resources/icons/scrollDown.svg new file mode 100644 index 0000000..65937ed --- /dev/null +++ b/src/main/resources/icons/scrollDown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/scrollDown_dark.svg b/src/main/resources/icons/scrollDown_dark.svg new file mode 100644 index 0000000..ff83c42 --- /dev/null +++ b/src/main/resources/icons/scrollDown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/scrollUp.svg b/src/main/resources/icons/scrollUp.svg new file mode 100644 index 0000000..a87131f --- /dev/null +++ b/src/main/resources/icons/scrollUp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/scrollUp_dark.svg b/src/main/resources/icons/scrollUp_dark.svg new file mode 100644 index 0000000..6aeece3 --- /dev/null +++ b/src/main/resources/icons/scrollUp_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/softWrap.svg b/src/main/resources/icons/softWrap.svg new file mode 100644 index 0000000..a4189c0 --- /dev/null +++ b/src/main/resources/icons/softWrap.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/softWrap_dark.svg b/src/main/resources/icons/softWrap_dark.svg new file mode 100644 index 0000000..56e7f65 --- /dev/null +++ b/src/main/resources/icons/softWrap_dark.svg @@ -0,0 +1,4 @@ + + + +