mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
241 lines
9.0 KiB
Kotlin
241 lines
9.0 KiB
Kotlin
package app.termora
|
|
|
|
import app.termora.database.DatabaseManager
|
|
import app.termora.plugin.ExtensionManager
|
|
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
|
import com.formdev.flatlaf.util.SystemInfo
|
|
import com.sun.jna.Pointer
|
|
import com.sun.jna.platform.WindowUtils
|
|
import com.sun.jna.platform.win32.User32
|
|
import com.sun.jna.platform.win32.WinDef
|
|
import com.sun.jna.platform.win32.WinUser.*
|
|
import de.jangassen.jfa.ThreadUtils
|
|
import de.jangassen.jfa.foundation.Foundation
|
|
import de.jangassen.jfa.foundation.ID
|
|
import org.slf4j.LoggerFactory
|
|
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
|
|
import javax.swing.UIManager
|
|
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
|
import kotlin.math.max
|
|
import kotlin.system.exitProcess
|
|
|
|
|
|
class TermoraFrameManager : Disposable {
|
|
|
|
companion object {
|
|
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
|
|
|
fun getInstance(): TermoraFrameManager {
|
|
return ApplicationScope.forApplicationScope()
|
|
.getOrCreate(TermoraFrameManager::class) { TermoraFrameManager() }
|
|
}
|
|
}
|
|
|
|
private val frames = mutableListOf<TermoraFrame>()
|
|
private val properties get() = DatabaseManager.getInstance().properties
|
|
private val isDisposed = AtomicBoolean(false)
|
|
private val isBackgroundRunning get() = DatabaseManager.getInstance().appearance.backgroundRunning
|
|
private val frameExtensions get() = ExtensionManager.getInstance().getExtensions(FrameExtension::class.java)
|
|
|
|
fun createWindow(): TermoraFrame {
|
|
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
|
frame.title = Application.getName()
|
|
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
|
|
|
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
|
if (rectangle.isMaximized) {
|
|
frame.setSize(1280, 800)
|
|
frame.setLocationRelativeTo(null)
|
|
frame.extendedState = rectangle.s
|
|
} else {
|
|
// 控制最小
|
|
frame.setSize(
|
|
max(rectangle.w, UIManager.getInt("Dialog.width") - 150),
|
|
max(rectangle.h, UIManager.getInt("Dialog.height") - 100)
|
|
)
|
|
if (rectangle.x == -1 && rectangle.y == -1) {
|
|
frame.setLocationRelativeTo(null)
|
|
} else {
|
|
frame.setLocation(max(rectangle.x, 0), max(rectangle.y, 0))
|
|
}
|
|
}
|
|
|
|
frame.addNotifyListener(object : NotifyListener {
|
|
private val opacity get() = DatabaseManager.getInstance().appearance.opacity
|
|
override fun addNotify() {
|
|
val opacity = this.opacity
|
|
if (opacity >= 1.0) return
|
|
setOpacity(frame, opacity)
|
|
}
|
|
})
|
|
|
|
for (extension in frameExtensions) {
|
|
extension.customize(frame)
|
|
}
|
|
|
|
return frame.apply { frames.add(this) }
|
|
}
|
|
|
|
fun getWindows(): Array<TermoraFrame> {
|
|
return frames.toTypedArray()
|
|
}
|
|
|
|
|
|
private fun registerCloseCallback(window: TermoraFrame) {
|
|
val manager = this
|
|
window.addWindowListener(object : WindowAdapter() {
|
|
override fun windowClosed(e: WindowEvent) {
|
|
|
|
// 销毁子窗口
|
|
TermoraRestarter.getInstance().disposeChildren(window)
|
|
|
|
// 存储位置信息
|
|
saveFrameRectangle(window)
|
|
|
|
// 删除
|
|
frames.remove(window)
|
|
|
|
// dispose windowScope
|
|
val windowScope = ApplicationScope.forWindowScope(e.window)
|
|
Disposer.disposeChildren(windowScope, null)
|
|
Disposer.dispose(windowScope)
|
|
|
|
val windowScopes = ApplicationScope.windowScopes()
|
|
if (windowScopes.isNotEmpty()) {
|
|
return
|
|
}
|
|
|
|
// 如果已经没有 Window 域了,那么就可以退出程序了
|
|
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) {
|
|
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()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fun tick() {
|
|
if (SwingUtilities.isEventDispatchThread()) {
|
|
val windows = getWindows()
|
|
if (windows.isEmpty()) return
|
|
for (window in windows) {
|
|
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
|
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
|
}
|
|
window.isVisible = true
|
|
}
|
|
windows.last().toFront()
|
|
} else {
|
|
SwingUtilities.invokeLater { tick() }
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
exitProcess(0)
|
|
}
|
|
|
|
private fun saveFrameRectangle(frame: TermoraFrame) {
|
|
properties.putString("TermoraFrame.x", frame.x.toString())
|
|
properties.putString("TermoraFrame.y", frame.y.toString())
|
|
properties.putString("TermoraFrame.width", frame.width.toString())
|
|
properties.putString("TermoraFrame.height", frame.height.toString())
|
|
properties.putString("TermoraFrame.extendedState", frame.extendedState.toString())
|
|
}
|
|
|
|
private fun getFrameRectangle(): FrameRectangle? {
|
|
val x = properties.getString("TermoraFrame.x")?.toIntOrNull() ?: return null
|
|
val y = properties.getString("TermoraFrame.y")?.toIntOrNull() ?: return null
|
|
val w = properties.getString("TermoraFrame.width")?.toIntOrNull() ?: return null
|
|
val h = properties.getString("TermoraFrame.height")?.toIntOrNull() ?: return null
|
|
val s = properties.getString("TermoraFrame.extendedState")?.toIntOrNull() ?: return null
|
|
return FrameRectangle(x, y, w, h, s)
|
|
}
|
|
|
|
fun setOpacity(opacity: Double) {
|
|
if (opacity < 0 || opacity > 1) return
|
|
for (window in getWindows()) {
|
|
setOpacity(window, opacity)
|
|
}
|
|
}
|
|
|
|
private fun setOpacity(window: Window, opacity: Double) {
|
|
if (SystemInfo.isMacOS) {
|
|
val nsWindow = ID(NativeMacLibrary.getNSWindow(window) ?: return)
|
|
ThreadUtils.dispatch_async {
|
|
Foundation.invoke(nsWindow, "setOpaque:", false)
|
|
Foundation.invoke(nsWindow, "setAlphaValue:", opacity)
|
|
}
|
|
} else if (SystemInfo.isWindows) {
|
|
val alpha = ((opacity * 255).toInt() and 0xFF).toByte()
|
|
val hwnd = WinDef.HWND(Pointer.createConstant(FlatNativeWindowsLibrary.getHWND(window)))
|
|
val exStyle = User32.INSTANCE.GetWindowLong(hwnd, GWL_EXSTYLE)
|
|
if (exStyle and WS_EX_LAYERED == 0) {
|
|
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
|
|
}
|
|
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
|
|
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
|
|
WindowUtils.setWindowAlpha(window, opacity.toFloat())
|
|
}
|
|
}
|
|
|
|
private data class FrameRectangle(
|
|
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
|
) {
|
|
val isMaximized get() = (s and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH
|
|
}
|
|
} |