feat: nvidia smi (#280)

This commit is contained in:
hstyi
2025-02-20 16:45:53 +08:00
committed by GitHub
parent 510324d7c4
commit 0000e4610a
15 changed files with 547 additions and 33 deletions

View File

@@ -29,6 +29,8 @@ object Icons {
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.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 add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
val locate by lazy { DynamicIcon("icons/locate.svg", "icons/locate_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 errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_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") } 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 listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_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 showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
val nvidia by lazy { DynamicIcon("icons/nvidia.svg", "icons/nvidia_dark.svg") }
} }

View File

@@ -80,7 +80,9 @@ class TermoraFrameManager {
try { try {
Disposer.getTree().assertIsEmpty(true) Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e.message) if (log.isErrorEnabled) {
log.error(e.message, e)
}
} }
exitProcess(0) exitProcess(0)

View File

@@ -6,6 +6,7 @@ import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.terminal.DataKey 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.SystemInformationVisualWindow
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.ui.FlatRoundBorder import com.formdev.flatlaf.ui.FlatRoundBorder
@@ -108,6 +109,9 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
// 服务器信息 // 服务器信息
add(initServerInfoActionButton()) add(initServerInfoActionButton())
// Nvidia 显卡信息
add(initNvidiaSMIActionButton())
// 重连 // 重连
add(initReconnectActionButton()) add(initReconnectActionButton())
@@ -143,6 +147,34 @@ class FloatingToolbarPanel : FlatToolBar(), Disposable {
return btn 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 { private fun initPinActionButton(): JButton {
val btn = JButton(Icons.pin) val btn = JButton(Icons.pin)
btn.isSelected = pinAction.isSelected btn.isSelected = pinAction.isSelected

View File

@@ -548,8 +548,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
override fun addVisualWindow(visualWindow: VisualWindow) { override fun addVisualWindow(visualWindow: VisualWindow) {
visualWindows = ArrayUtils.add(visualWindows, visualWindow) visualWindows = ArrayUtils.add(visualWindows, visualWindow)
layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any) layeredPane.add(visualWindow.getJComponent(), JLayeredPane.DRAG_LAYER as Any)
layeredPane.revalidate() layeredPane.moveToFront(visualWindow.getJComponent())
layeredPane.repaint()
} }
override fun removeVisualWindow(visualWindow: VisualWindow) { override fun removeVisualWindow(visualWindow: VisualWindow) {

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -1,8 +1,6 @@
package app.termora.terminal.panel.vw package app.termora.terminal.panel.vw
import app.termora.* import app.termora.*
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -11,15 +9,14 @@ import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.session.ClientSession import org.apache.sshd.client.session.ClientSession
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) : class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
VisualWindowPanel("SystemInformation", visualWindowManager) { SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
companion object { companion object {
private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java) private val log = LoggerFactory.getLogger(SystemInformationVisualWindow::class.java)
@@ -41,22 +38,6 @@ class SystemInformationVisualWindow(private val tab: SSHTerminalTab, visualWindo
private fun initEvents() { private fun initEvents() {
Disposer.register(this, systemInformationPanel) 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 { private inner class SystemInformationPanel : JPanel(BorderLayout()), Disposable {

View File

@@ -28,6 +28,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
private var dialog: VisualWindowDialog? = null private var dialog: VisualWindowDialog? = null
private var oldBounds = Rectangle() private var oldBounds = Rectangle()
private var toggleWindowBtn = JButton(Icons.openInNewWindow) 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() { private val closeWindowListener = object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
close() 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) 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() { 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() { 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(JLabel(Icons.empty))
toolbar.add(Box.createHorizontalGlue()) toolbar.add(Box.createHorizontalGlue())
toolbar.add(titleLabel) toolbar.add(titleLabel)
toolbar.add(Box.createHorizontalGlue()) toolbar.add(Box.createHorizontalGlue())
btns.forEach { toolbar.add(it) }
toolbar.add(toggleWindowBtn) toolbar.add(toggleWindowBtn)
toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } }) toolbar.add(JButton(Icons.close).apply { addActionListener { Disposer.dispose(visualWindow) } })
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor) 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?.dispose()
dialog = null dialog = null
alwaysTopBtn.isVisible = isWindow
if (isWindow) { if (isWindow) {
oldBounds = bounds oldBounds = bounds
// 变基 // 变基
@@ -212,13 +240,11 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
protected open fun close() { protected open fun close() {
SwingUtilities.invokeLater { if (isWindow()) {
if (isWindow()) { dialog?.dispose()
dialog?.dispose() dialog = null
dialog = null
}
visualWindowManager.removeVisualWindow(visualWindow)
} }
visualWindowManager.removeVisualWindow(visualWindow)
} }
private inner class VisualWindowDialog : DialogWrapper(null) { private inner class VisualWindowDialog : DialogWrapper(null) {
@@ -228,6 +254,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
controlsVisible = false controlsVisible = false
isResizable = true isResizable = true
title = getWindowTitle() title = getWindowTitle()
isAlwaysOnTop = isAlwaysTop
initEvents() initEvents()
@@ -251,8 +278,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
} }
private fun initEvents() { private fun initEvents() {
Disposer.register(disposable, object : Disposable { addWindowListener(object : WindowAdapter() {
override fun dispose() { override fun windowClosed(e: WindowEvent) {
properties.putString("VisualWindow.${id}.dialog.location.x", x.toString()) properties.putString("VisualWindow.${id}.dialog.location.x", x.toString())
properties.putString("VisualWindow.${id}.dialog.location.y", y.toString()) properties.putString("VisualWindow.${id}.dialog.location.y", y.toString())
properties.putString("VisualWindow.${id}.dialog.location.width", width.toString()) properties.putString("VisualWindow.${id}.dialog.location.width", width.toString())

View File

@@ -350,6 +350,9 @@ termora.visual-window.system-information.filesystem=Filesystem
termora.visual-window.system-information.used-total=Used / Total termora.visual-window.system-information.used-total=Used / Total
termora.visual-window.nvidia-smi=NVIDIA System Management Interface
termora.floating-toolbar.not-supported=This action is not supported termora.floating-toolbar.not-supported=This action is not supported

View File

@@ -0,0 +1,5 @@
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
width="16" height="16">
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
p-id="9517" fill="#6C707E"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
<svg t="1740039296619" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516"
width="16" height="16">
<path d="M381.781333 375.381333v-61.013333a285.866667 285.866667 0 0 1 18.090667-0.768c167.338667-5.290667 277.034667 143.957333 277.034667 143.957333s-118.357333 164.309333-245.333334 164.309334a156.586667 156.586667 0 0 1-49.408-7.893334v-185.429333c65.194667 7.893333 78.378667 36.565333 117.205334 101.76l87.04-73.130667s-63.658667-83.285333-170.666667-83.285333a256.682667 256.682667 0 0 0-33.962667 1.493333m0-202.026666v91.221333l18.090667-1.152c232.533333-7.893333 384.426667 190.72 384.426667 190.72s-174.08 211.797333-355.413334 211.797333a275.626667 275.626667 0 0 1-46.72-4.138666v56.533333c12.8 1.493333 26.026667 2.645333 38.826667 2.645333 168.832 0 290.986667-86.314667 409.301333-188.074666 19.584 15.829333 99.84 53.888 116.48 70.485333-112.341333 94.208-374.272 169.984-522.794666 169.984-14.293333 0-27.861333-0.768-41.429334-2.261333v79.530666H1024V173.354667z m0 440.576v48.256c-156.032-27.904-199.381333-190.293333-199.381333-190.293334s75.008-82.944 199.381333-96.512v52.778667H381.44c-65.194667-7.936-116.48 53.12-116.48 53.12s29.013333 102.912 116.864 132.693333M104.789333 465.066667s92.330667-136.405333 277.333334-150.741334V264.576C177.194667 281.173333 0 454.528 0 454.528s100.266667 290.218667 381.781333 316.586667v-52.778667c-206.506667-25.6-276.992-253.269333-276.992-253.269333z"
p-id="9517" fill="#CED0D6"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
width="16" height="16">
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
p-id="5142" fill="#6C707E"></path>
</svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -0,0 +1,5 @@
<svg t="1740037914635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5141"
width="16" height="16">
<path d="M855.7 210.8l-42.4-42.4c-3.1-3.1-8.2-3.1-11.3 0L168.3 801.9c-3.1 3.1-3.1 8.2 0 11.3l42.4 42.4c3.1 3.1 8.2 3.1 11.3 0L855.6 222c3.2-3 3.2-8.1 0.1-11.2zM304 448c79.4 0 144-64.6 144-144s-64.6-144-144-144-144 64.6-144 144 64.6 144 144 144z m0-216c39.7 0 72 32.3 72 72s-32.3 72-72 72-72-32.3-72-72 32.3-72 72-72zM720 576c-79.4 0-144 64.6-144 144s64.6 144 144 144 144-64.6 144-144-64.6-144-144-144z m0 216c-39.7 0-72-32.3-72-72s32.3-72 72-72 72 32.3 72 72-32.3 72-72 72z"
p-id="5142" fill="#CED0D6"></path>
</svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -0,0 +1,5 @@
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
width="16" height="16">
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
fill="#6C707E" p-id="7037"></path>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,5 @@
<svg t="1740038430861" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7036"
width="16" height="16">
<path d="M853.333333 138.666667H170.666667c-17.066667 0-32 14.933333-32 32v128c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V202.666667h277.333333v618.666666H384c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32h-96v-618.666666h277.333333V298.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V170.666667c0-17.066667-14.933333-32-32-32z"
fill="#CED0D6" p-id="7037"></path>
</svg>

After

Width:  |  Height:  |  Size: 609 B