mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
feat: nvidia smi (#280)
This commit is contained in:
@@ -29,6 +29,8 @@ object Icons {
|
||||
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 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") }
|
||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||
@@ -117,5 +119,6 @@ object Icons {
|
||||
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
||||
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
||||
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
||||
val nvidia by lazy { DynamicIcon("icons/nvidia.svg", "icons/nvidia_dark.svg") }
|
||||
|
||||
}
|
||||
@@ -80,7 +80,9 @@ class TermoraFrameManager {
|
||||
try {
|
||||
Disposer.getTree().assertIsEmpty(true)
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
exitProcess(0)
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.panel.vw.NvidiaSMIVisualWindow
|
||||
import app.termora.terminal.panel.vw.SystemInformationVisualWindow
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.ui.FlatRoundBorder
|
||||
@@ -108,6 +109,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
// 服务器信息
|
||||
add(initServerInfoActionButton())
|
||||
|
||||
// Nvidia 显卡信息
|
||||
add(initNvidiaSMIActionButton())
|
||||
|
||||
// 重连
|
||||
add(initReconnectActionButton())
|
||||
|
||||
@@ -143,6 +147,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
|
||||
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 = evt.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)
|
||||
|
||||
}
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
private fun initPinActionButton(): JButton {
|
||||
val btn = JButton(Icons.pin)
|
||||
btn.isSelected = pinAction.isSelected
|
||||
|
||||
@@ -548,8 +548,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
override fun addVisualWindow(visualWindow: VisualWindow) {
|
||||
visualWindows = ArrayUtils.add(visualWindows, visualWindow)
|
||||
layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any)
|
||||
layeredPane.revalidate()
|
||||
layeredPane.repaint()
|
||||
layeredPane.moveToFront(visualWindow.getJComponent())
|
||||
}
|
||||
|
||||
override fun removeVisualWindow(visualWindow: VisualWindow) {
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.SshClients
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXBusyLabel
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.xml.sax.EntityResolver
|
||||
import org.xml.sax.InputSource
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridLayout
|
||||
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) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(NvidiaSMIVisualWindow::class.java)
|
||||
}
|
||||
|
||||
private val nvidiaSMIPanel by lazy { NvidiaSMIPanel() }
|
||||
private val busyLabel = JXBusyLabel()
|
||||
private val errorPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref, 5dlu, pref"))
|
||||
.add(JLabel(FlatSVGIcon(Icons.warningDialog.name, 60, 60))).xy(1, 2, "center, fill")
|
||||
.add(JLabel("Not supported")).xy(1, 4, "center, fill")
|
||||
.build()
|
||||
private val loadingPanel = FormBuilder.create().layout(FormLayout("pref:grow", "20dlu, pref"))
|
||||
.add(busyLabel).xy(1, 2, "center, fill")
|
||||
.build()
|
||||
private val cardLayout = CardLayout()
|
||||
private val rootPanel = JPanel(cardLayout)
|
||||
private var isPercentage
|
||||
get() = properties.getString("VisualWindow.${id}.isPercentage", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.isPercentage", value.toString())
|
||||
|
||||
private val percentageBtn by lazy { JButton(if (isPercentage) Icons.text else Icons.percentage) }
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
initVisualWindowPanel()
|
||||
}
|
||||
|
||||
override fun toolbarButtons(): List<JButton> {
|
||||
return listOf(percentageBtn)
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
title = I18n.getString("termora.visual-window.nvidia-smi")
|
||||
busyLabel.isBusy = true
|
||||
|
||||
rootPanel.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
rootPanel.add(errorPanel, "ErrorPanel")
|
||||
rootPanel.add(loadingPanel, "LoadingPanel")
|
||||
rootPanel.add(nvidiaSMIPanel, "NvidiaSMIPanel")
|
||||
|
||||
add(rootPanel, BorderLayout.CENTER)
|
||||
|
||||
cardLayout.show(rootPanel, "LoadingPanel")
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
percentageBtn.addActionListener {
|
||||
isPercentage = !isPercentage
|
||||
percentageBtn.icon = if (isPercentage) Icons.text else Icons.percentage
|
||||
nvidiaSMIPanel.refreshPanel()
|
||||
}
|
||||
}
|
||||
|
||||
private data class GPU(
|
||||
/**
|
||||
* 名称 product_name
|
||||
*/
|
||||
val productName: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 序号 minor_number
|
||||
*/
|
||||
val minorNumber: Int = 0,
|
||||
|
||||
/**
|
||||
* 温度 temperature.gpu_temp
|
||||
*
|
||||
* 单位:C
|
||||
*/
|
||||
var temp: Double = 0.0,
|
||||
var tempText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的功率 gpu_power_readings.power_draw
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerUsage: Double = 0.0,
|
||||
var powerUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 功率大小 gpu_power_readings.max_power_limit
|
||||
*
|
||||
* 单位:W
|
||||
*/
|
||||
var powerCap: Double = 0.0,
|
||||
var powerCapText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 使用的显存 fb_memory_usage.used
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryUsage: Double = 0.0,
|
||||
var memoryUsageText: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 显存大小 fb_memory_usage.total
|
||||
*
|
||||
* 单位:Mib
|
||||
*/
|
||||
var memoryCap: Double = 0.0,
|
||||
var memoryCapText: String = StringUtils.EMPTY,
|
||||
|
||||
|
||||
/**
|
||||
* GPU 利用率 utilization.gpu_util
|
||||
*
|
||||
* 单位:%
|
||||
*/
|
||||
var gpu: Double = 0.0
|
||||
)
|
||||
|
||||
private class NvidiaSMI(
|
||||
val driverVersion: String = StringUtils.EMPTY,
|
||||
val cudaVersion: String = StringUtils.EMPTY,
|
||||
val gpus: MutableList<GPU> = mutableListOf<GPU>(),
|
||||
)
|
||||
|
||||
private class GPUPanel(val minorNumber: Int, title: String) : JPanel(BorderLayout()) {
|
||||
val gpuProgressBar = SmartProgressBar()
|
||||
val tempProgressBar = SmartProgressBar()
|
||||
val memProgressBar = SmartProgressBar()
|
||||
val powerProgressBar = SmartProgressBar()
|
||||
|
||||
init {
|
||||
val formMargin = "4dlu"
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val p = FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
)
|
||||
.border(
|
||||
BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createTitledBorder(title),
|
||||
BorderFactory.createEmptyBorder(4, 4, 4, 4),
|
||||
)
|
||||
)
|
||||
.add("GPU: ").xy(1, rows)
|
||||
.add(gpuProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Temp: ").xy(1, rows)
|
||||
.add(tempProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Mem: ").xy(1, rows)
|
||||
.add(memProgressBar).xy(3, rows).apply { rows += step }
|
||||
.add("Power: ").xy(1, rows)
|
||||
.add(powerProgressBar).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
add(p, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class NvidiaSMIPanel : JPanel(BorderLayout()) {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val xPath by lazy { XPathFactory.newInstance().newXPath() }
|
||||
private val db by lazy {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
factory.isValidating = false
|
||||
factory.isXIncludeAware = false
|
||||
factory.isNamespaceAware = false
|
||||
val db = factory.newDocumentBuilder()
|
||||
db.setEntityResolver(object : EntityResolver {
|
||||
override fun resolveEntity(
|
||||
publicId: String?,
|
||||
systemId: String?
|
||||
): InputSource? {
|
||||
return if (StringUtils.contains(systemId, ".dtd")) {
|
||||
InputSource(StringReader(StringUtils.EMPTY))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
db
|
||||
}
|
||||
|
||||
private var nvidiaSMI = NvidiaSMI()
|
||||
private val gpuRootPanel = JPanel()
|
||||
private val driverVersionLabel = JLabel()
|
||||
private val cudaVersionLabel = JLabel()
|
||||
private val gpusLabel = JLabel()
|
||||
|
||||
|
||||
init {
|
||||
initViews()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
|
||||
add(
|
||||
FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"default:grow, pref, default:grow, 4dlu, pref, default:grow, 4dlu, pref, default:grow, default:grow",
|
||||
"pref, 4dlu"
|
||||
)
|
||||
)
|
||||
.add(Box.createHorizontalGlue()).xy(1, 1)
|
||||
.add("Driver: ").xy(2, 1)
|
||||
.add(driverVersionLabel).xy(3, 1)
|
||||
.add("CUDA: ").xy(5, 1)
|
||||
.add(cudaVersionLabel).xy(6, 1)
|
||||
.add("GPUS: ").xy(8, 1)
|
||||
.add(gpusLabel).xy(9, 1)
|
||||
.add(Box.createHorizontalGlue()).xy(10, 1)
|
||||
.build(), BorderLayout.NORTH
|
||||
)
|
||||
|
||||
add(JScrollPane(gpuRootPanel).apply {
|
||||
verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||
verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||
verticalScrollBar.minimumSize = Dimension(0, 0)
|
||||
border = BorderFactory.createEmptyBorder()
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
coroutineScope.launch {
|
||||
|
||||
// 首次刷新
|
||||
refresh(true)
|
||||
|
||||
while (coroutineScope.isActive) {
|
||||
delay(1000.milliseconds)
|
||||
|
||||
try {
|
||||
refresh()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun refresh(isFirst: Boolean = false) {
|
||||
val session = tab.getData(SSHTerminalTab.SSHSession) ?: return
|
||||
|
||||
val doc = try {
|
||||
val (code, text) = SshClients.execChannel(session, "nvidia-smi -x -q")
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
db.parse(InputSource(StringReader(text)))
|
||||
} else {
|
||||
throw IllegalStateException("exit code: $code")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
if (doc == null) {
|
||||
if (isFirst) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
cardLayout.show(rootPanel, "ErrorPanel")
|
||||
}
|
||||
// 直接取消轮训
|
||||
SwingUtilities.invokeLater { coroutineScope.cancel() }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nvidiaSMI = NvidiaSMI(
|
||||
driverVersion = xPath.compile("/nvidia_smi_log/driver_version/text()").evaluate(doc),
|
||||
cudaVersion = xPath.compile("/nvidia_smi_log/cuda_version/text()").evaluate(doc),
|
||||
)
|
||||
val attachedGPUs = xPath.compile("/nvidia_smi_log/attached_gpus/text()").evaluate(doc).toIntOrNull() ?: 0
|
||||
|
||||
for (i in 1..attachedGPUs) {
|
||||
val gpu = GPU(
|
||||
productName = xPath.compile("/nvidia_smi_log/gpu[${i}]/product_name/text()").evaluate(doc),
|
||||
minorNumber = xPath.compile("/nvidia_smi_log/gpu[${i}]/minor_number/text()").evaluate(doc)
|
||||
.toIntOrNull() ?: 0,
|
||||
tempText = xPath.compile("/nvidia_smi_log/gpu[${i}]/temperature/gpu_temp/text()").evaluate(doc),
|
||||
powerUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/power_draw/text()")
|
||||
.evaluate(doc),
|
||||
powerCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/gpu_power_readings/max_power_limit/text()")
|
||||
.evaluate(doc),
|
||||
memoryUsageText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/used/text()")
|
||||
.evaluate(doc),
|
||||
memoryCapText = xPath.compile("/nvidia_smi_log/gpu[${i}]/fb_memory_usage/total/text()")
|
||||
.evaluate(doc),
|
||||
gpu = xPath.compile("/nvidia_smi_log/gpu[${i}]/utilization/gpu_util/text()")
|
||||
.evaluate(doc).split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
|
||||
nvidiaSMI.gpus.add(
|
||||
gpu.copy(
|
||||
temp = gpu.tempText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerUsage = gpu.powerUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
powerCap = gpu.powerCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryUsage = gpu.memoryUsageText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
memoryCap = gpu.memoryCapText.split(StringUtils.SPACE).first().toDoubleOrNull() ?: 0.0,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (isFirst) {
|
||||
initPanel()
|
||||
cardLayout.show(rootPanel, "NvidiaSMIPanel")
|
||||
}
|
||||
|
||||
refreshPanel()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initPanel() {
|
||||
gpuRootPanel.layout = GridLayout(
|
||||
if (nvidiaSMI.gpus.size % 2 == 0) nvidiaSMI.gpus.size / 2 else nvidiaSMI.gpus.size / 2 + 1,
|
||||
2, 4, 4
|
||||
)
|
||||
for (e in nvidiaSMI.gpus) {
|
||||
gpuRootPanel.add(GPUPanel(e.minorNumber, "${e.minorNumber} ${e.productName}"))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshPanel() {
|
||||
cudaVersionLabel.text = nvidiaSMI.cudaVersion
|
||||
driverVersionLabel.text = nvidiaSMI.driverVersion
|
||||
gpusLabel.text = nvidiaSMI.gpus.size.toString()
|
||||
|
||||
for (c in gpuRootPanel.components) {
|
||||
if (c is GPUPanel) {
|
||||
for (g in nvidiaSMI.gpus) {
|
||||
if (c.minorNumber == g.minorNumber) {
|
||||
refreshGPUPanel(g, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshGPUPanel(gpu: GPU, g: GPUPanel) {
|
||||
g.gpuProgressBar.value = gpu.gpu.toInt()
|
||||
|
||||
g.tempProgressBar.value = gpu.temp.toInt()
|
||||
g.tempProgressBar.string = if (isPercentage) "${g.tempProgressBar.value}%" else gpu.tempText
|
||||
|
||||
g.powerProgressBar.value = (gpu.powerUsage / gpu.powerCap * 100.0).toInt()
|
||||
g.powerProgressBar.string = if (isPercentage) "${g.powerProgressBar.value}%"
|
||||
else "${gpu.powerUsageText}/${gpu.powerCapText}"
|
||||
|
||||
g.memProgressBar.value = (gpu.memoryUsage / gpu.memoryCap * 100.0).toInt()
|
||||
g.memProgressBar.string = if (isPercentage) "${g.memProgressBar.value}%"
|
||||
else "${gpu.memoryUsageText}/${gpu.memoryCapText}"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
busyLabel.isBusy = false
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
abstract class SSHVisualWindow(
|
||||
protected val tab: SSHTerminalTab,
|
||||
id: String,
|
||||
visualWindowManager: VisualWindowManager
|
||||
) : VisualWindowPanel(id, visualWindowManager) {
|
||||
|
||||
init {
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
super.toggleWindow()
|
||||
|
||||
if (!isWindow()) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getWindowTitle(): String {
|
||||
return tab.getTitle() + " - " + title
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora.terminal.panel.vw
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.*
|
||||
@@ -11,15 +9,14 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.client.session.ClientSession
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
VisualWindowPanel("SystemInformation", visualWindowManager) {
|
||||
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java)
|
||||
@@ -41,22 +38,6 @@ class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindo
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(this, systemInformationPanel)
|
||||
Disposer.register(tab, this)
|
||||
}
|
||||
|
||||
override fun getWindowTitle(): String {
|
||||
return tab.getTitle() + " - " + title
|
||||
}
|
||||
|
||||
override fun toggleWindow() {
|
||||
val evt = AnActionEvent(tab.getJComponent(), StringUtils.EMPTY, EventObject(this))
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
super.toggleWindow()
|
||||
|
||||
if (!isWindow()) {
|
||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SystemInformationPanel : JPanel(BorderLayout()), Disposable {
|
||||
|
||||
@@ -28,6 +28,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
private var dialog: VisualWindowDialog? = null
|
||||
private var oldBounds = Rectangle()
|
||||
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
||||
private var isAlwaysTop
|
||||
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
||||
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
||||
|
||||
private val alwaysTopBtn = JButton(Icons.moveUp)
|
||||
private val closeWindowListener = object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
close()
|
||||
@@ -64,6 +69,12 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
|
||||
if (w > 0 && h > 0) setSize(w, h) else setSize(400, 200)
|
||||
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
alwaysTopBtn.isVisible = false
|
||||
}
|
||||
|
||||
protected open fun toolbarButtons(): List<JButton> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -102,14 +113,29 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
alwaysTopBtn.addActionListener {
|
||||
isAlwaysTop = !isAlwaysTop
|
||||
alwaysTopBtn.isSelected = isAlwaysTop
|
||||
|
||||
if (isWindow()) {
|
||||
dialog?.isAlwaysOnTop = isAlwaysTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initToolBar() {
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
val btns = toolbarButtons()
|
||||
val count = 2 + btns.size
|
||||
toolbar.add(alwaysTopBtn)
|
||||
toolbar.add(Box.createHorizontalStrut(count * 26))
|
||||
toolbar.add(JLabel(Icons.empty))
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(titleLabel)
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
btns.forEach { toolbar.add(it) }
|
||||
|
||||
toolbar.add(toggleWindowBtn)
|
||||
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
|
||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||
@@ -157,6 +183,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
|
||||
alwaysTopBtn.isVisible = isWindow
|
||||
|
||||
if (isWindow) {
|
||||
oldBounds = bounds
|
||||
// 变基
|
||||
@@ -212,13 +240,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
|
||||
|
||||
protected open fun close() {
|
||||
SwingUtilities.invokeLater {
|
||||
if (isWindow()) {
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
}
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
if (isWindow()) {
|
||||
dialog?.dispose()
|
||||
dialog = null
|
||||
}
|
||||
visualWindowManager.removeVisualWindow(visualWindow)
|
||||
}
|
||||
|
||||
private inner class VisualWindowDialog : DialogWrapper(null) {
|
||||
@@ -228,6 +254,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
controlsVisible = false
|
||||
isResizable = true
|
||||
title = getWindowTitle()
|
||||
isAlwaysOnTop = isAlwaysTop
|
||||
|
||||
initEvents()
|
||||
|
||||
@@ -251,8 +278,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(disposable, object : Disposable {
|
||||
override fun dispose() {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
properties.putString("VisualWindow.${id}.dialog.location.x", x.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.y", y.toString())
|
||||
properties.putString("VisualWindow.${id}.dialog.location.width", width.toString())
|
||||
|
||||
Reference in New Issue
Block a user