diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 5e3a104..caff885 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -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") } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt index 4e7742b..862a498 100644 --- a/src/main/kotlin/app/termora/TermoraFrameManager.kt +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -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) diff --git a/src/main/kotlin/app/termora/terminal/panel/FloatingToolbarPanel.kt b/src/main/kotlin/app/termora/terminal/panel/FloatingToolbarPanel.kt index c9f2408..db3c013 100644 --- a/src/main/kotlin/app/termora/terminal/panel/FloatingToolbarPanel.kt +++ b/src/main/kotlin/app/termora/terminal/panel/FloatingToolbarPanel.kt @@ -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 diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt index 39aeb01..c52f34c 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt @@ -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) { diff --git a/src/main/kotlin/app/termora/terminal/panel/vw/NvidiaSMIVisualWindow.kt b/src/main/kotlin/app/termora/terminal/panel/vw/NvidiaSMIVisualWindow.kt new file mode 100644 index 0000000..d18d44e --- /dev/null +++ b/src/main/kotlin/app/termora/terminal/panel/vw/NvidiaSMIVisualWindow.kt @@ -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 { + 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 = mutableListOf(), + ) + + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/vw/SSHVisualWindow.kt b/src/main/kotlin/app/termora/terminal/panel/vw/SSHVisualWindow.kt new file mode 100644 index 0000000..2846965 --- /dev/null +++ b/src/main/kotlin/app/termora/terminal/panel/vw/SSHVisualWindow.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/vw/SystemInformationVisualWindow.kt b/src/main/kotlin/app/termora/terminal/panel/vw/SystemInformationVisualWindow.kt index da4346d..24ab7b4 100644 --- a/src/main/kotlin/app/termora/terminal/panel/vw/SystemInformationVisualWindow.kt +++ b/src/main/kotlin/app/termora/terminal/panel/vw/SystemInformationVisualWindow.kt @@ -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 { diff --git a/src/main/kotlin/app/termora/terminal/panel/vw/VisualWindowPanel.kt b/src/main/kotlin/app/termora/terminal/panel/vw/VisualWindowPanel.kt index d637246..df4429c 100644 --- a/src/main/kotlin/app/termora/terminal/panel/vw/VisualWindowPanel.kt +++ b/src/main/kotlin/app/termora/terminal/panel/vw/VisualWindowPanel.kt @@ -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 { + 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()) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 29345cb..b89138b 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -350,6 +350,9 @@ termora.visual-window.system-information.filesystem=Filesystem 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 diff --git a/src/main/resources/icons/nvidia.svg b/src/main/resources/icons/nvidia.svg new file mode 100644 index 0000000..23a5209 --- /dev/null +++ b/src/main/resources/icons/nvidia.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/nvidia_dark.svg b/src/main/resources/icons/nvidia_dark.svg new file mode 100644 index 0000000..bc72d9a --- /dev/null +++ b/src/main/resources/icons/nvidia_dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/percentage.svg b/src/main/resources/icons/percentage.svg new file mode 100644 index 0000000..ea26f3b --- /dev/null +++ b/src/main/resources/icons/percentage.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/percentage_dark.svg b/src/main/resources/icons/percentage_dark.svg new file mode 100644 index 0000000..90f841c --- /dev/null +++ b/src/main/resources/icons/percentage_dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/text.svg b/src/main/resources/icons/text.svg new file mode 100644 index 0000000..ec73ff4 --- /dev/null +++ b/src/main/resources/icons/text.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/text_dark.svg b/src/main/resources/icons/text_dark.svg new file mode 100644 index 0000000..f82deeb --- /dev/null +++ b/src/main/resources/icons/text_dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file