From 0c5b6f8112e79c81993148c251993e9dbba8c5f5 Mon Sep 17 00:00:00 2001 From: hstyi Date: Thu, 27 Mar 2025 17:22:13 +0800 Subject: [PATCH] feat: support system tray (#403) --- .gitignore | 1 + .../kotlin/app/termora/ApplicationRunner.kt | 41 +++++++++++++++++++ src/main/kotlin/app/termora/Database.kt | 5 +++ .../kotlin/app/termora/SettingsOptionsPane.kt | 26 +++++++++--- .../kotlin/app/termora/TermoraFrameManager.kt | 23 +++++++---- src/main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../resources/i18n/messages_zh_TW.properties | 1 + 8 files changed, 86 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index a273c0c..5701e69 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ certs/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.vs ### IntelliJ IDEA ### .idea diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index c89f9d7..5757d38 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -21,7 +21,13 @@ import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.SystemUtils import org.json.JSONObject import org.slf4j.LoggerFactory +import java.awt.MenuItem +import java.awt.PopupMenu +import java.awt.SystemTray +import java.awt.TrayIcon +import java.awt.event.ActionEvent import java.util.* +import javax.imageio.ImageIO import javax.swing.* import kotlin.system.exitProcess import kotlin.system.measureTimeMillis @@ -63,6 +69,9 @@ class ApplicationRunner { // 启动主窗口 val startMainFrame = measureTimeMillis { startMainFrame() } + // 设置托盘 + val setupSystemTray = measureTimeMillis { SwingUtilities.invokeLater { setupSystemTray() } } + if (log.isDebugEnabled) { log.debug("printSystemInfo: {}ms", printSystemInfo) log.debug("openDatabase: {}ms", openDatabase) @@ -71,6 +80,7 @@ class ApplicationRunner { log.debug("setupLaf: {}ms", setupLaf) log.debug("openDoor: {}ms", openDoor) log.debug("startMainFrame: {}ms", startMainFrame) + log.debug("setupSystemTray: {}ms", setupSystemTray) } }.let { if (log.isDebugEnabled) { @@ -106,6 +116,37 @@ class ApplicationRunner { } } + private fun setupSystemTray() { + if (!SystemInfo.isWindows || !SystemTray.isSupported()) return + + val tray = SystemTray.getSystemTray() + val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png")) + val trayIcon = TrayIcon(image) + val popupMenu = PopupMenu() + trayIcon.popupMenu = popupMenu + trayIcon.toolTip = Application.getName() + + // PopupMenu 不支持中文 + val exitMenu = MenuItem("Exit") + exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } } + popupMenu.add(exitMenu) + + // double click + trayIcon.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + TermoraFrameManager.getInstance().tick() + } + }) + + tray.add(trayIcon) + + Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable { + override fun dispose() { + tray.remove(trayIcon) + } + }) + } + private fun quitHandler() { for (frame in TermoraFrameManager.getInstance().getWindows()) { frame.dispose() diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index 50a7a42..aac7653 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -580,6 +580,11 @@ class Database private constructor(private val env: Environment) : Disposable { var darkTheme by StringPropertyDelegate("Dark") var lightTheme by StringPropertyDelegate("Light") + /** + * 允许后台运行,也就是托盘 + */ + var backgroundRunning by BooleanPropertyDelegate(false) + /** * 语言 */ diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 92cfcde..b436136 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -129,6 +129,7 @@ class SettingsOptionsPane : OptionsPane() { val themeManager = ThemeManager.getInstance() val themeComboBox = FlatComboBox() val languageComboBox = FlatComboBox() + val backgroundComBoBox = YesOrNoComboBox() val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val preferredThemeBtn = JButton(Icons.settings) private val appearance get() = database.appearance @@ -142,6 +143,7 @@ class SettingsOptionsPane : OptionsPane() { followSystemCheckBox.isSelected = appearance.followSystem preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected + backgroundComBoBox.selectedItem = appearance.backgroundRunning themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeManager.themes.keys.forEach { themeComboBox.addItem(it) } @@ -178,6 +180,12 @@ class SettingsOptionsPane : OptionsPane() { } } + backgroundComBoBox.addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + appearance.backgroundRunning = backgroundComBoBox.selectedItem as Boolean + } + } + followSystemCheckBox.addActionListener { appearance.followSystem = followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected @@ -276,7 +284,7 @@ class SettingsOptionsPane : OptionsPane() { private fun getFormPanel(): JPanel { val layout = FormLayout( "left:pref, $formMargin, default:grow, $formMargin, default, default:grow", - "pref, $formMargin, pref, $formMargin" + "pref, $formMargin, pref, $formMargin, pref" ) val box = FlatToolBar() box.add(followSystemCheckBox) @@ -285,7 +293,7 @@ class SettingsOptionsPane : OptionsPane() { var rows = 1 val step = 2 - return FormBuilder.create().layout(layout) + val builder = FormBuilder.create().layout(layout) .add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows) .add(themeComboBox).xy(3, rows) .add(box).xy(5, rows).apply { rows += step } @@ -296,7 +304,13 @@ class SettingsOptionsPane : OptionsPane() { Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n")) } })).xy(5, rows).apply { rows += step } - .build() + + if (SystemInfo.isWindows) { + builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows) + .add(backgroundComBoBox).xy(3, rows) + } + + return builder.build() } @@ -408,8 +422,8 @@ class SettingsOptionsPane : OptionsPane() { } private fun fireFontChanged() { - TerminalPanelFactory.getInstance() - .fireResize() + TerminalPanelFactory.getInstance() + .fireResize() } private fun initView() { @@ -470,7 +484,7 @@ class SettingsOptionsPane : OptionsPane() { shellComboBox.selectedItem = terminalSetting.localShell - val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced") + val fonts = linkedSetOf("JetBrains Mono", "Source Code Pro", "Monospaced") FontUtils.getAllFonts().forEach { if (!fonts.contains(it.family)) { fonts.addLast(it.family) diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt index a0bde58..2df0e16 100644 --- a/src/main/kotlin/app/termora/TermoraFrameManager.kt +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -26,6 +26,7 @@ class TermoraFrameManager { private val frames = mutableListOf() private val properties get() = Database.getDatabase().properties + private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning fun createWindow(): TermoraFrame { val frame = TermoraFrame().apply { registerCloseCallback(this) } @@ -83,13 +84,20 @@ class TermoraFrameManager { override fun windowClosing(e: WindowEvent) { if (ApplicationScope.windowScopes().size == 1) { - if (OptionPane.showConfirmDialog( - window, - I18n.getString("termora.quit-confirm", Application.getName()), - optionType = JOptionPane.YES_NO_OPTION, - ) == JOptionPane.YES_OPTION - ) { - window.dispose() + if (SystemInfo.isWindows && isBackgroundRunning) { + // 最小化 + window.extendedState = window.extendedState or JFrame.ICONIFIED + // 隐藏 + window.isVisible = false + } else { + if (OptionPane.showConfirmDialog( + window, + I18n.getString("termora.quit-confirm", Application.getName()), + optionType = JOptionPane.YES_NO_OPTION, + ) == JOptionPane.YES_OPTION + ) { + window.dispose() + } } } else { window.dispose() @@ -106,6 +114,7 @@ class TermoraFrameManager { if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) { window.extendedState = window.extendedState and JFrame.ICONIFIED.inv() } + window.isVisible = true } windows.last().toFront() } else { diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 6683f08..d779a19 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -50,6 +50,7 @@ termora.settings.appearance.theme=Theme termora.settings.appearance.language=Language termora.settings.appearance.i-want-to-translate=I want to translate termora.settings.appearance.follow-system=Sync with OS +termora.settings.appearance.background-running=Backgrounding termora.setting.security=Security termora.setting.security.enter-password=Enter password diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 78abb16..44ff742 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -48,6 +48,7 @@ termora.settings.appearance.theme=主题 termora.settings.appearance.language=语言 termora.settings.appearance.i-want-to-translate=我想要翻译 termora.settings.appearance.follow-system=跟随系统 +termora.settings.appearance.background-running=后台运行 termora.setting.security=安全 termora.setting.security.enter-password=请输入密码 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 55574c3..4bd1f62 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -49,6 +49,7 @@ termora.settings.appearance.theme=主题 termora.settings.appearance.language=語言 termora.settings.appearance.i-want-to-translate=我想要翻譯 termora.settings.appearance.follow-system=跟隨系統 +termora.settings.appearance.background-running=後台運行 termora.setting.security=安全 termora.setting.security.enter-password=請輸入密碼