feat: use extension for floating toolbar

This commit is contained in:
hstyi
2025-07-03 11:40:21 +08:00
committed by hstyi
parent c0ecc9fa7d
commit 9ce4a88041
16 changed files with 300 additions and 131 deletions

View File

@@ -37,7 +37,7 @@ record ExtensionProxy(Plugin plugin, Extension extension) implements InvocationH
}
throw new IllegalCallerException(target.getMessage(), target);
}
throw e;
throw target;
}
}
}

View File

@@ -23,7 +23,10 @@ abstract class PtyHostTerminalTab(
private var readerJob: Job? = null
private val ptyConnectorDelegate = PtyConnectorDelegate()
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(terminal, ptyConnectorDelegate)
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(
this,
terminal, ptyConnectorDelegate
)
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
override fun start() {

View File

@@ -38,9 +38,9 @@ class TerminalPanelFactory : Disposable {
}
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
fun createTerminalPanel(tab: TerminalTab?, terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
val writer = MyTerminalWriter(ptyConnector)
val terminalPanel = TerminalPanel(terminal, writer)
val terminalPanel = TerminalPanel(tab, terminal, writer)
// processDeviceStatusReport
terminal.getTerminalModel().setData(DataKey.TerminalWriter, writer)

View File

@@ -82,8 +82,8 @@ class TermoraFrame : JFrame(), DataProvider {
if (scope != windowScope) return emptyList()
var filter = hostTreeModel.root.getAllChildren()
.map { it.host }
.filter { it.isFolder.not() }
.map { it.host }
if (pattern.isNotBlank()) {
filter = filter.filter {

View File

@@ -1,5 +1,6 @@
package app.termora.actions
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.BoundAction
import java.awt.event.ActionEvent
import javax.swing.Icon
@@ -20,7 +21,7 @@ abstract class AnAction : BoundAction {
if (evt is AnActionEvent) {
actionPerformed(evt)
} else {
actionPerformed(AnActionEvent(evt.source, evt.actionCommand, evt))
actionPerformed(AnActionEvent(evt.source, StringUtils.defaultString(evt.actionCommand), evt))
}
}

View File

@@ -40,7 +40,7 @@ class SSHCopyIdDialog(
}
}
private val terminalPanel by lazy {
terminalPanelFactory.createTerminalPanel(terminal, PtyConnectorDelegate())
terminalPanelFactory.createTerminalPanel(null, terminal, PtyConnectorDelegate())
.apply { enableFloatingToolbar = false }
}
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

View File

@@ -13,6 +13,7 @@ import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
import app.termora.transfer.internal.local.LocalPlugin
import app.termora.transfer.internal.sftp.SFTPPlugin
import com.formdev.flatlaf.util.SystemInfo
@@ -127,6 +128,9 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(LocalPlugin(), origin = PluginOrigin.Internal, version = version))
// sftp transfer plugin
plugins.add(PluginDescriptor(SFTPPlugin(), origin = PluginOrigin.Internal, version = version))
// floating
plugins.add(PluginDescriptor(FloatingToolbarPlugin(), origin = PluginOrigin.Internal, version = version))
}
private fun loadSystemPlugins() {

View File

@@ -0,0 +1,20 @@
package app.termora.terminal.panel
import app.termora.TerminalTab
import app.termora.actions.AnAction
import app.termora.plugin.Extension
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
interface FloatingToolbarActionExtension : Extension {
/**
* 抛出 [UnsupportedOperationException] 表示不支持
*/
fun createActionButton(visualWindowManager: VisualWindowManager, tab: TerminalTab): AnAction
/**
* 获取要返回的虚拟窗口
*/
fun getVisualWindowClass(tab: TerminalTab): Class<out VisualWindow>
}

View File

@@ -6,13 +6,10 @@ import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider
import app.termora.actions.DataProviders
import app.termora.database.DatabaseManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.ssh.SSHTerminalTab
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
import app.termora.terminal.panel.vw.TransferVisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.ui.FlatRoundBorder
import org.apache.commons.lang3.StringUtils
@@ -26,7 +23,7 @@ import javax.swing.SwingUtilities
class FloatingToolbarPanel : FlatToolBar(), Disposable {
private val floatingToolbarEnable get() = DatabaseManager.getInstance().terminal.floatingToolbar
private var closed = false
private val anEvent get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
private val event get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
companion object {
@@ -79,7 +76,6 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
}
}
initActions()
initEvents()
}
@@ -116,26 +112,36 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// Pin
add(initPinActionButton())
// 服务器信息
add(initServerInfoActionButton())
val tab = event.getData(DataProviders.TerminalTab)
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel)
if (terminalPanel != null) {
val extensions = ExtensionManager.getInstance()
.getExtensions(FloatingToolbarActionExtension::class.java)
for (extension in extensions) {
try {
add(createButton(extension.createActionButton(terminalPanel, tab), terminalPanel, tab, extension))
} catch (_: UnsupportedOperationException) {
continue
}
}
// Transfer
add(initTransferActionButton())
initReconnectActionButton(tab)
}
// Snippet
add(initSnippetActionButton())
// Nvidia 显卡信息
add(initNvidiaSMIActionButton())
// 重连
add(initReconnectActionButton())
// 关闭
add(initCloseActionButton())
}
private fun initEvents() {
// 初始化 Action
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
override fun propertyChange(evt: PropertyChangeEvent) {
removePropertyChangeListener("ancestor", this)
initActions()
}
})
// 被添加到组件后
addPropertyChangeListener("ancestor", object : PropertyChangeListener {
override fun propertyChange(evt: PropertyChangeEvent) {
@@ -143,11 +149,12 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
SwingUtilities.invokeLater { resumeVisualWindows() }
}
})
}
@Suppress("UNCHECKED_CAST")
private fun resumeVisualWindows() {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val tab = event.getData(DataProviders.TerminalTab) ?: return
if (tab !is SSHTerminalTab) return
val terminalPanel = tab.getData(DataProviders.TerminalPanel) ?: return
terminalPanel.resumeVisualWindows(tab.host.id, object : DataProvider {
@@ -160,106 +167,30 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
})
}
private fun initServerInfoActionButton(): JButton {
val btn = JButton(Icons.infoOutline)
btn.toolTipText = I18n.getString("termora.visual-window.system-information")
btn.addActionListener(object : AnAction() {
private fun createButton(
action: AnAction,
visualWindowManager: VisualWindowManager,
tab: TerminalTab,
extension: FloatingToolbarActionExtension
): JButton {
val btn = JButton(object : AnAction(action.smallIcon) {
override fun actionPerformed(evt: AnActionEvent) {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
if (tab !is SSHTerminalTab) {
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
return
}
for (window in terminalPanel.getVisualWindows()) {
if (window is SystemInformationVisualWindow) {
terminalPanel.moveToFront(window)
return
try {
val clazz = extension.getVisualWindowClass(tab)
for (window in visualWindowManager.getVisualWindows()) {
if (clazz.isInstance(window)) {
visualWindowManager.moveToFront(window)
return
}
}
action.actionPerformed(evt)
} catch (_: UnsupportedOperationException) {
action.actionPerformed(evt)
}
val visualWindowPanel = SystemInformationVisualWindow(tab, terminalPanel)
terminalPanel.addVisualWindow(visualWindowPanel)
}
})
return btn
}
private fun initTransferActionButton(): JButton {
val btn = JButton(Icons.folder)
btn.toolTipText = I18n.getString("termora.transport.sftp")
btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
if (tab !is SSHTerminalTab) {
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
return
}
for (window in terminalPanel.getVisualWindows()) {
if (window is TransferVisualWindow) {
terminalPanel.moveToFront(window)
return
}
}
val visualWindowPanel = TransferVisualWindow(tab, terminalPanel)
terminalPanel.addVisualWindow(visualWindowPanel)
}
})
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 = anEvent.getData(DataProviders.TerminalTab) ?: return
val writer = tab.getData(DataProviders.TerminalWriter) ?: 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, writer)
}
})
return btn
}
private fun initNvidiaSMIActionButton(): JButton {
val btn = JButton(Icons.nvidia)
btn.toolTipText = I18n.getString("termora.visual-window.nvidia-smi")
btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
val terminalPanel = (tab as DataProvider?)?.getData(DataProviders.TerminalPanel) ?: return
if (tab !is SSHTerminalTab) {
terminalPanel.toast(I18n.getString("termora.floating-toolbar.not-supported"))
return
}
for (window in terminalPanel.getVisualWindows()) {
if (window is NvidiaSMIVisualWindow) {
terminalPanel.moveToFront(window)
return
}
}
val visualWindowPanel = NvidiaSMIVisualWindow(tab, terminalPanel)
terminalPanel.addVisualWindow(visualWindowPanel)
}
})
btn.text = StringUtils.EMPTY
btn.toolTipText = action.shortDescription
return btn
}
@@ -293,19 +224,16 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
return btn
}
private fun initReconnectActionButton(): JButton {
private fun initReconnectActionButton(tab: TerminalTab) {
if (tab.canReconnect().not()) return
val btn = JButton(Icons.refresh)
btn.toolTipText = I18n.getString("termora.tabbed.contextmenu.reconnect")
btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val tab = anEvent.getData(DataProviders.TerminalTab) ?: return
if (tab.canReconnect()) {
tab.reconnect()
}
tab.reconnect()
}
})
return btn
add(btn)
}
}

View File

@@ -2,6 +2,7 @@ package app.termora.terminal.panel
import app.termora.Disposable
import app.termora.Disposer
import app.termora.TerminalTab
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
@@ -35,7 +36,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter) :
class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val writer: TerminalWriter) :
JPanel(BorderLayout()), DataProvider, Disposable, VisualWindowManager {
companion object {
@@ -554,7 +555,13 @@ class TerminalPanel(val terminal: Terminal, private val writer: TerminalWriter)
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == DataProviders.TerminalTab) {
if (tab != null) {
return tab as T
}
}
return dataProviderSupport.getData(dataKey)
}

View File

@@ -0,0 +1,27 @@
package app.termora.terminal.panel.vw
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
import app.termora.terminal.panel.FloatingToolbarActionExtension
import app.termora.terminal.panel.vw.extensions.NvidiaVisualWindowActionExtension
import app.termora.terminal.panel.vw.extensions.ServerInfoVisualWindowActionExtension
import app.termora.terminal.panel.vw.extensions.SnippetVisualWindowActionExtension
import app.termora.terminal.panel.vw.extensions.TransferVisualWindowActionExtension
internal class FloatingToolbarPlugin : InternalPlugin() {
init {
support.addExtension(FloatingToolbarActionExtension::class.java) { TransferVisualWindowActionExtension.instance }
support.addExtension(FloatingToolbarActionExtension::class.java) { ServerInfoVisualWindowActionExtension.instance }
support.addExtension(FloatingToolbarActionExtension::class.java) { SnippetVisualWindowActionExtension.instance }
support.addExtension(FloatingToolbarActionExtension::class.java) { NvidiaVisualWindowActionExtension.instance }
}
override fun getName(): String {
return "FloatingToolbar"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -10,6 +10,7 @@ import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.apache.commons.lang3.StringUtils
@@ -25,6 +26,7 @@ import java.io.StringReader
import javax.swing.*
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathFactory
import kotlin.time.Duration.Companion.milliseconds
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
@@ -264,7 +266,14 @@ class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWind
override suspend fun refresh(isFirst: Boolean) {
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
val session = suspend {
var c = tab.getData(SSHTerminalTab.SSHSession)
while (c == null) {
delay(250.milliseconds)
c = tab.getData(SSHTerminalTab.SSHSession)
}
c
}.invoke()
val doc = try {
val (code, text) = SshClients.execChannel(session, "nvidia-smi -x -q")

View File

@@ -0,0 +1,41 @@
package app.termora.terminal.panel.vw.extensions
import app.termora.I18n
import app.termora.Icons
import app.termora.TerminalTab
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.terminal.panel.FloatingToolbarActionExtension
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
class NvidiaVisualWindowActionExtension private constructor() : FloatingToolbarActionExtension {
companion object {
val instance = NvidiaVisualWindowActionExtension()
}
override fun createActionButton(visualWindowManager: VisualWindowManager, tab: TerminalTab): AnAction {
if (tab !is SSHTerminalTab) throw UnsupportedOperationException()
return object : AnAction(Icons.nvidia) {
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.visual-window.nvidia-smi"))
}
override fun actionPerformed(evt: AnActionEvent) {
val visualWindowPanel = NvidiaSMIVisualWindow(tab, visualWindowManager)
visualWindowManager.addVisualWindow(visualWindowPanel)
}
}
}
override fun getVisualWindowClass(tab: TerminalTab): Class<out VisualWindow> {
return NvidiaSMIVisualWindow::class.java
}
override fun ordered(): Long {
return 3;
}
}

View File

@@ -0,0 +1,42 @@
package app.termora.terminal.panel.vw.extensions
import app.termora.I18n
import app.termora.Icons
import app.termora.TerminalTab
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.terminal.panel.FloatingToolbarActionExtension
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
class ServerInfoVisualWindowActionExtension private constructor() : FloatingToolbarActionExtension {
companion object {
val instance = ServerInfoVisualWindowActionExtension()
}
override fun createActionButton(visualWindowManager: VisualWindowManager, tab: TerminalTab): AnAction {
if (tab !is SSHTerminalTab) throw UnsupportedOperationException()
return object : AnAction(Icons.infoOutline) {
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.visual-window.system-information"))
}
override fun actionPerformed(evt: AnActionEvent) {
val visualWindowPanel = SystemInformationVisualWindow(tab, visualWindowManager)
visualWindowManager.addVisualWindow(visualWindowPanel)
}
}
}
override fun getVisualWindowClass(tab: TerminalTab): Class<out VisualWindow> {
if (tab !is SSHTerminalTab) throw UnsupportedOperationException()
return SystemInformationVisualWindow::class.java
}
override fun ordered(): Long {
return -1;
}
}

View File

@@ -0,0 +1,50 @@
package app.termora.terminal.panel.vw.extensions
import app.termora.I18n
import app.termora.Icons
import app.termora.PtyHostTerminalTab
import app.termora.TerminalTab
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.snippet.SnippetAction
import app.termora.snippet.SnippetTreeDialog
import app.termora.terminal.panel.FloatingToolbarActionExtension
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
import javax.swing.JComponent
class SnippetVisualWindowActionExtension private constructor() : FloatingToolbarActionExtension {
companion object {
val instance = SnippetVisualWindowActionExtension()
}
override fun createActionButton(visualWindowManager: VisualWindowManager, tab: TerminalTab): AnAction {
if (tab !is PtyHostTerminalTab) throw UnsupportedOperationException()
return object : AnAction(Icons.codeSpan) {
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.snippet.title"))
}
override fun actionPerformed(evt: AnActionEvent) {
val btn = evt.source as? JComponent ?: return
val writer = tab.getData(DataProviders.TerminalWriter) ?: return
val dialog = SnippetTreeDialog(evt.window)
dialog.setLocationRelativeTo(btn)
dialog.setLocation(dialog.x, btn.locationOnScreen.y + btn.height + 2)
dialog.isVisible = true
val node = dialog.getSelectedNode() ?: return
SnippetAction.getInstance().runSnippet(node.data, writer)
}
}
}
override fun getVisualWindowClass(tab: TerminalTab): Class<out VisualWindow> {
throw UnsupportedOperationException()
}
override fun ordered(): Long {
return 2;
}
}

View File

@@ -0,0 +1,37 @@
package app.termora.terminal.panel.vw.extensions
import app.termora.Icons
import app.termora.TerminalTab
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.terminal.panel.FloatingToolbarActionExtension
import app.termora.terminal.panel.vw.TransferVisualWindow
import app.termora.terminal.panel.vw.VisualWindow
import app.termora.terminal.panel.vw.VisualWindowManager
class TransferVisualWindowActionExtension private constructor() : FloatingToolbarActionExtension {
companion object {
val instance = TransferVisualWindowActionExtension()
}
override fun createActionButton(visualWindowManager: VisualWindowManager, tab: TerminalTab): AnAction {
if (tab !is SSHTerminalTab) throw UnsupportedOperationException()
return object : AnAction(Icons.folder) {
override fun actionPerformed(evt: AnActionEvent) {
val visualWindowPanel = TransferVisualWindow(tab, visualWindowManager)
visualWindowManager.addVisualWindow(visualWindowPanel)
}
}
}
override fun getVisualWindowClass(tab: TerminalTab): Class<out VisualWindow> {
if (tab !is SSHTerminalTab) throw UnsupportedOperationException()
return TransferVisualWindow::class.java
}
override fun ordered(): Long {
return 1
}
}