feat: support system tray (#403)

This commit is contained in:
hstyi
2025-03-27 17:22:13 +08:00
committed by GitHub
parent 7c26e3d08a
commit 0c5b6f8112
8 changed files with 86 additions and 13 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ certs/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.vs
### IntelliJ IDEA ###
.idea

View File

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

View File

@@ -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)
/**
* 语言
*/

View File

@@ -129,6 +129,7 @@ class SettingsOptionsPane : OptionsPane() {
val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>()
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<String>("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)

View File

@@ -26,6 +26,7 @@ class TermoraFrameManager {
private val frames = mutableListOf<TermoraFrame>()
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 {

View File

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

View File

@@ -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=请输入密码

View File

@@ -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=請輸入密碼