feat: macOS supports running in the background (#487)

This commit is contained in:
hstyi
2025-04-10 14:47:01 +08:00
committed by GitHub
parent da9b6c21d6
commit 6b48f577e9
4 changed files with 103 additions and 30 deletions

View File

@@ -28,8 +28,12 @@ import java.awt.MenuItem
import java.awt.PopupMenu
import java.awt.SystemTray
import java.awt.TrayIcon
import java.awt.desktop.AppReopenedEvent
import java.awt.desktop.AppReopenedListener
import java.awt.desktop.SystemEventListener
import java.awt.event.ActionEvent
import java.util.*
import java.util.concurrent.CountDownLatch
import javax.imageio.ImageIO
import javax.swing.*
import kotlin.system.exitProcess
@@ -123,7 +127,20 @@ class ApplicationRunner {
TermoraFrameManager.getInstance().createWindow().isVisible = true
if (SystemUtils.IS_OS_MAC_OSX) {
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
SwingUtilities.invokeLater {
try {
// 设置 Dock
setupMacOSDock()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
// Command + Q
FlatDesktop.setQuitHandler { quitHandler() }
}
}
}
@@ -159,9 +176,13 @@ class ApplicationRunner {
}
private fun quitHandler() {
for (frame in TermoraFrameManager.getInstance().getWindows()) {
val windows = TermoraFrameManager.getInstance().getWindows()
for (frame in windows) {
frame.dispose()
}
Disposer.dispose(TermoraFrameManager.getInstance())
}
private fun loadSettings() {
@@ -243,7 +264,35 @@ class ApplicationRunner {
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
}
private fun setupMacOSDock() {
val countDownLatch = CountDownLatch(1)
val cls = Class.forName("com.apple.eawt.Application")
val app = cls.getMethod("getApplication").invoke(null)
val addAppEventListener = cls.getMethod("addAppEventListener", SystemEventListener::class.java)
addAppEventListener.invoke(app, object : AppReopenedListener {
override fun appReopened(e: AppReopenedEvent) {
val manager = TermoraFrameManager.getInstance()
if (manager.getWindows().isEmpty()) {
manager.createWindow().isVisible = true
}
}
})
// 当应用程序销毁时,驻守线程也可以退出了
Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable {
override fun dispose() {
countDownLatch.countDown()
}
})
// 驻守线程,不然当所有窗口都关闭时,程序会自动退出
// wait application exit
Thread.ofPlatform().daemon(false)
.priority(Thread.MIN_PRIORITY)
.start { countDownLatch.await() }
}
private fun printSystemInfo() {

View File

@@ -151,6 +151,7 @@ class ApplicationScope private constructor() : Scope() {
}
fun windowScopes(): List<WindowScope> {
if (scopes.isEmpty()) return emptyList()
return scopes.values.toList()
}

View File

@@ -148,7 +148,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = FilenameUtils.getName(appearance.backgroundImage)

View File

@@ -15,6 +15,7 @@ import java.awt.Frame
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JFrame
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
@@ -24,7 +25,7 @@ import kotlin.math.max
import kotlin.system.exitProcess
class TermoraFrameManager {
class TermoraFrameManager : Disposable {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
@@ -37,6 +38,7 @@ class TermoraFrameManager {
private val frames = mutableListOf<TermoraFrame>()
private val properties get() = Database.getDatabase().properties
private val isDisposed = AtomicBoolean(false)
private val isBackgroundRunning get() = Database.getDatabase().appearance.backgroundRunning
fun createWindow(): TermoraFrame {
@@ -80,6 +82,7 @@ class TermoraFrameManager {
private fun registerCloseCallback(window: TermoraFrame) {
val manager = this
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
@@ -95,31 +98,49 @@ class TermoraFrameManager {
Disposer.dispose(windowScope)
val windowScopes = ApplicationScope.windowScopes()
if (windowScopes.isNotEmpty()) {
return
}
// 如果已经没有 Window 域了,那么就可以退出程序了
if (windowScopes.isEmpty()) {
this@TermoraFrameManager.dispose()
if (SystemInfo.isWindows || SystemInfo.isLinux) {
Disposer.dispose(manager)
} else if (SystemInfo.isMacOS) {
// 如果 macOS 开启了后台运行,那么尽管所有窗口都没了,也不会退出
if (isBackgroundRunning) {
return
}
Disposer.dispose(manager)
}
}
override fun windowClosing(e: WindowEvent) {
if (ApplicationScope.windowScopes().size == 1) {
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 {
if (ApplicationScope.windowScopes().size != 1) {
window.dispose()
return
}
// 如果 Windows 开启了后台运行,那么最小化
if (SystemInfo.isWindows && isBackgroundRunning) {
// 最小化
window.extendedState = window.extendedState or JFrame.ICONIFIED
// 隐藏
window.isVisible = false
return
}
// 如果 macOS 已经开启了后台运行,那么直接销毁,因为会有一个进程驻守
if (SystemInfo.isMacOS && isBackgroundRunning) {
window.dispose()
return
}
val option = OptionPane.showConfirmDialog(
window,
I18n.getString("termora.quit-confirm", Application.getName()),
optionType = JOptionPane.YES_NO_OPTION,
)
if (option == JOptionPane.YES_OPTION) {
window.dispose()
}
}
@@ -142,14 +163,16 @@ class TermoraFrameManager {
}
}
private fun dispose() {
Disposer.dispose(ApplicationScope.forApplicationScope())
override fun dispose() {
if (isDisposed.compareAndSet(false, true)) {
Disposer.dispose(ApplicationScope.forApplicationScope())
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}