From 6b48f577e9d9cfedb3832895aace468a85826ccf Mon Sep 17 00:00:00 2001 From: hstyi Date: Thu, 10 Apr 2025 14:47:01 +0800 Subject: [PATCH] feat: macOS supports running in the background (#487) --- .../kotlin/app/termora/ApplicationRunner.kt | 53 ++++++++++++- src/main/kotlin/app/termora/Scope.kt | 1 + .../kotlin/app/termora/SettingsOptionsPane.kt | 2 +- .../kotlin/app/termora/TermoraFrameManager.kt | 77 ++++++++++++------- 4 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index ab8a7f0..faa309f 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -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() { diff --git a/src/main/kotlin/app/termora/Scope.kt b/src/main/kotlin/app/termora/Scope.kt index 75f4020..f028e2a 100644 --- a/src/main/kotlin/app/termora/Scope.kt +++ b/src/main/kotlin/app/termora/Scope.kt @@ -151,6 +151,7 @@ class ApplicationScope private constructor() : Scope() { } fun windowScopes(): List { + if (scopes.isEmpty()) return emptyList() return scopes.values.toList() } diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 0c595e6..d15da3d 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -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) diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt index 4936a80..2df6f7c 100644 --- a/src/main/kotlin/app/termora/TermoraFrameManager.kt +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -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() 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) + } } }