From 744e64b3595630e7cc8488288f54cb46ed5f883d Mon Sep 17 00:00:00 2001 From: hstyi Date: Tue, 1 Apr 2025 14:57:57 +0800 Subject: [PATCH] feat: support to set transparency (#446) --- src/main/kotlin/app/termora/Database.kt | 12 +++++ src/main/kotlin/app/termora/NotifyListener.kt | 7 +++ .../kotlin/app/termora/SettingsOptionsPane.kt | 38 +++++++++++++--- src/main/kotlin/app/termora/TermoraFrame.kt | 15 +++++++ .../kotlin/app/termora/TermoraFrameManager.kt | 45 +++++++++++++++++++ src/main/kotlin/app/termora/TextField.kt | 2 +- src/main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../resources/i18n/messages_zh_TW.properties | 1 + 9 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/app/termora/NotifyListener.kt diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index a099fc5..f4cde19 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -421,6 +421,13 @@ class Database private constructor(private val env: Environment) : Disposable { } } + protected inner class DoublePropertyDelegate(defaultValue: Double) : + PropertyDelegate(defaultValue) { + override fun convertValue(value: String): Double { + return value.toDoubleOrNull() ?: initializer.invoke() + } + } + protected inner class LongPropertyDelegate(defaultValue: Long) : PropertyDelegate(defaultValue) { @@ -632,6 +639,11 @@ class Database private constructor(private val env: Environment) : Disposable { I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString() } + + /** + * 透明度 + */ + var opacity by DoublePropertyDelegate(1.0) } /** diff --git a/src/main/kotlin/app/termora/NotifyListener.kt b/src/main/kotlin/app/termora/NotifyListener.kt new file mode 100644 index 0000000..7f09bf3 --- /dev/null +++ b/src/main/kotlin/app/termora/NotifyListener.kt @@ -0,0 +1,7 @@ +package app.termora + +import java.util.* + +interface NotifyListener : EventListener { + fun addNotify() +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index f1b86d7..934e58e 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -61,6 +61,7 @@ import java.nio.charset.StandardCharsets import java.util.* import java.util.function.Consumer import javax.swing.* +import javax.swing.JSpinner.NumberEditor import javax.swing.event.DocumentEvent import javax.swing.event.PopupMenuEvent import javax.swing.event.PopupMenuListener @@ -131,6 +132,8 @@ class SettingsOptionsPane : OptionsPane() { val backgroundComBoBox = YesOrNoComboBox() val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val preferredThemeBtn = JButton(Icons.settings) + val opacitySpinner = NumberSpinner(100, 0, 100) + private val appearance get() = database.appearance init { @@ -140,6 +143,21 @@ class SettingsOptionsPane : OptionsPane() { private fun initView() { + backgroundComBoBox.isEnabled = SystemInfo.isWindows + + opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows + opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) { + override fun getNextValue(): Any { + return super.getNextValue() ?: maximum + } + + override fun getPreviousValue(): Any { + return super.getPreviousValue() ?: minimum + } + } + opacitySpinner.editor = NumberEditor(opacitySpinner, "#.##") + opacitySpinner.model.stepSize = 0.05 + followSystemCheckBox.isSelected = appearance.followSystem preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected backgroundComBoBox.selectedItem = appearance.backgroundRunning @@ -179,6 +197,14 @@ class SettingsOptionsPane : OptionsPane() { } } + opacitySpinner.addChangeListener { + val opacity = opacitySpinner.value + if (opacity is Double) { + TermoraFrameManager.getInstance().setOpacity(opacity) + appearance.opacity = opacity + } + } + backgroundComBoBox.addItemListener { if (it.stateChange == ItemEvent.SELECTED) { appearance.backgroundRunning = backgroundComBoBox.selectedItem as Boolean @@ -283,7 +309,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" + "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" ) val box = FlatToolBar() box.add(followSystemCheckBox) @@ -304,10 +330,12 @@ class SettingsOptionsPane : OptionsPane() { } })).xy(5, rows).apply { rows += step } - if (SystemInfo.isWindows) { - builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows) - .add(backgroundComBoBox).xy(3, rows) - } + + builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows) + .add(opacitySpinner).xy(3, rows).apply { rows += step } + + builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows) + .add(backgroundComBoBox).xy(3, rows) return builder.build() } diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index 056a69b..6d423b3 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -9,6 +9,7 @@ import app.termora.terminal.DataKey import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR +import org.apache.commons.lang3.ArrayUtils import java.awt.BorderLayout import java.awt.Dimension import java.awt.Insets @@ -24,6 +25,7 @@ import javax.swing.SwingUtilities import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.UIManager + fun assertEventDispatchThread() { if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue") } @@ -41,6 +43,7 @@ class TermoraFrame : JFrame(), DataProvider { private val welcomePanel = WelcomePanel(windowScope) private val sftp get() = Database.getDatabase().sftp private val myUI = MyFlatRootPaneUI() + private var notifyListeners = emptyArray() init { @@ -239,4 +242,16 @@ class TermoraFrame : JFrame(), DataProvider { return id.hashCode() } + fun addNotifyListener(listener: NotifyListener) { + notifyListeners += listener + } + + fun removeNotifyListener(listener: NotifyListener) { + notifyListeners = ArrayUtils.removeElements(notifyListeners, listener) + } + + override fun addNotify() { + super.addNotify() + notifyListeners.forEach { it.addNotify() } + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt index 2df0e16..4936a80 100644 --- a/src/main/kotlin/app/termora/TermoraFrameManager.kt +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -1,8 +1,18 @@ package app.termora +import app.termora.native.osx.NativeMacLibrary +import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary import com.formdev.flatlaf.util.SystemInfo +import com.sun.jna.Pointer +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 javax.swing.JFrame @@ -13,6 +23,7 @@ import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE import kotlin.math.max import kotlin.system.exitProcess + class TermoraFrameManager { companion object { @@ -51,6 +62,15 @@ class TermoraFrameManager { } } + frame.addNotifyListener(object : NotifyListener { + private val opacity get() = Database.getDatabase().appearance.opacity + override fun addNotify() { + val opacity = this.opacity + if (opacity >= 1.0) return + setOpacity(frame, opacity) + } + }) + return frame.apply { frames.add(this) } } @@ -153,6 +173,31 @@ class TermoraFrameManager { return FrameRectangle(x, y, w, h, s) } + fun setOpacity(opacity: Double) { + if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) 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, User32.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) + } + } + private data class FrameRectangle( val x: Int, val y: Int, val w: Int, val h: Int, val s: Int ) { diff --git a/src/main/kotlin/app/termora/TextField.kt b/src/main/kotlin/app/termora/TextField.kt index e9c7f8f..41ae663 100644 --- a/src/main/kotlin/app/termora/TextField.kt +++ b/src/main/kotlin/app/termora/TextField.kt @@ -146,7 +146,7 @@ open class EmailFormattedTextField(var maxLength: Int = Int.MAX_VALUE) : Outline } -abstract class NumberSpinner( +open class NumberSpinner( value: Int, minimum: Int, maximum: Int, diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index baabc60..b2a0449 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -54,6 +54,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.opacity=Opacity termora.settings.appearance.background-running=Backgrounding termora.setting.security=Security diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 95d40b7..ff04528 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -51,6 +51,7 @@ termora.settings.appearance.theme=主题 termora.settings.appearance.language=语言 termora.settings.appearance.i-want-to-translate=我想要翻译 termora.settings.appearance.follow-system=跟随系统 +termora.settings.appearance.opacity=透明度 termora.settings.appearance.background-running=后台运行 termora.setting.security=安全 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 8393f57..86c7fcd 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -52,6 +52,7 @@ termora.settings.appearance.theme=主题 termora.settings.appearance.language=語言 termora.settings.appearance.i-want-to-translate=我想要翻譯 termora.settings.appearance.follow-system=跟隨系統 +termora.settings.appearance.opacity=透明度 termora.settings.appearance.background-running=後台運行 termora.setting.security=安全