feat: support setting background image (#475)

This commit is contained in:
hstyi
2025-04-09 16:03:38 +08:00
committed by GitHub
parent 95846ab135
commit dddbb49084
9 changed files with 176 additions and 19 deletions

View File

@@ -64,6 +64,9 @@ class ApplicationRunner {
fileSystemManager.filesCache = WeakRefFilesCache() fileSystemManager.filesCache = WeakRefFilesCache()
fileSystemManager.init() fileSystemManager.init()
VFS.setManager(fileSystemManager) VFS.setManager(fileSystemManager)
// async init
BackgroundManager.getInstance().getBackgroundImage()
} }
// 设置 LAF // 设置 LAF

View File

@@ -0,0 +1,72 @@
package app.termora
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import javax.swing.SwingUtilities
class BackgroundManager private constructor() {
companion object {
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
fun getInstance(): BackgroundManager {
return ApplicationScope.forApplicationScope().getOrCreate(BackgroundManager::class) { BackgroundManager() }
}
}
private val appearance get() = Database.getDatabase().appearance
private var bufferedImage: BufferedImage? = null
private var imageFilepath = StringUtils.EMPTY
fun setBackgroundImage(file: File) {
synchronized(this) {
try {
bufferedImage = ImageIO.read(file)
imageFilepath = file.absolutePath
appearance.backgroundImage = file.absolutePath
SwingUtilities.invokeLater {
for (window in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.updateComponentTreeUI(window)
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
fun getBackgroundImage(): BufferedImage? {
synchronized(this) {
if (bufferedImage == null || imageFilepath.isEmpty()) {
if (appearance.backgroundImage.isBlank()) {
return null
}
val file = File(appearance.backgroundImage)
if (file.exists()) {
setBackgroundImage(file)
}
}
return bufferedImage
}
}
fun clearBackgroundImage() {
synchronized(this) {
bufferedImage = null
imageFilepath = StringUtils.EMPTY
appearance.backgroundImage = StringUtils.EMPTY
SwingUtilities.invokeLater {
for (window in TermoraFrameManager.getInstance().getWindows()) {
SwingUtilities.updateComponentTreeUI(window)
}
}
}
}
}

View File

@@ -643,6 +643,11 @@ class Database private constructor(private val env: Environment) : Disposable {
*/ */
var backgroundRunning by BooleanPropertyDelegate(false) var backgroundRunning by BooleanPropertyDelegate(false)
/**
* 背景图片的地址
*/
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
/** /**
* 语言 * 语言
*/ */

View File

@@ -1,11 +0,0 @@
package app.termora
import com.formdev.flatlaf.ui.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane
class MyFlatRootPaneUI : FlatRootPaneUI() {
fun getTitlePane(): FlatTitlePane? {
return super.titlePane
}
}

View File

@@ -39,6 +39,8 @@ import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
@@ -132,8 +134,11 @@ class SettingsOptionsPane : OptionsPane() {
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings) val preferredThemeBtn = JButton(Icons.settings)
val opacitySpinner = NumberSpinner(100, 0, 100) val opacitySpinner = NumberSpinner(100, 0, 100)
val backgroundImageTextField = OutlineTextField()
private val appearance get() = database.appearance private val appearance get() = database.appearance
private val backgroundButton = JButton(Icons.folder)
private val backgroundClearButton = FlatButton()
init { init {
initView() initView()
@@ -143,6 +148,14 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() { private fun initView() {
backgroundComBoBox.isEnabled = SystemInfo.isWindows backgroundComBoBox.isEnabled = SystemInfo.isWindows
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = FilenameUtils.getName(appearance.backgroundImage)
backgroundClearButton.isFocusable = false
backgroundClearButton.icon = Icons.delete
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) { opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
@@ -239,6 +252,45 @@ class SettingsOptionsPane : OptionsPane() {
} }
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() } preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
backgroundButton.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
chooser.showOpenDialog(owner).thenAccept {
if (it.isNotEmpty()) {
onSelectedBackgroundImage(it.first())
}
}
}
backgroundClearButton.addActionListener {
BackgroundManager.getInstance().clearBackgroundImage()
backgroundImageTextField.text = StringUtils.EMPTY
}
}
private fun onSelectedBackgroundImage(file: File) {
try {
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
FileUtils.forceMkdirParent(destFile)
FileUtils.copyFile(file, destFile)
backgroundImageTextField.text = destFile.name
BackgroundManager.getInstance().setBackgroundImage(destFile)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
SwingUtilities.invokeLater {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -308,7 +360,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow", "left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
) )
val box = FlatToolBar() val box = FlatToolBar()
box.add(followSystemCheckBox) box.add(followSystemCheckBox)
@@ -330,6 +382,13 @@ class SettingsOptionsPane : OptionsPane() {
})).xy(5, rows).apply { rows += step } })).xy(5, rows).apply { rows += step }
val bgClearBox = Box.createHorizontalBox()
bgClearBox.add(backgroundClearButton)
builder.add("${I18n.getString("termora.settings.appearance.background-image")}:").xy(1, rows)
.add(backgroundImageTextField).xy(3, rows)
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows) builder.add("${I18n.getString("termora.settings.appearance.opacity")}:").xy(1, rows)
.add(opacitySpinner).xy(3, rows).apply { rows += step } .add(opacitySpinner).xy(3, rows).apply { rows += step }

View File

@@ -7,12 +7,13 @@ import app.termora.actions.DataProviders
import app.termora.sftp.SFTPTab import app.termora.sftp.SFTPTab
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.ui.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import java.awt.BorderLayout import java.awt.*
import java.awt.Dimension
import java.awt.Insets
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.awt.event.MouseListener import java.awt.event.MouseListener
@@ -42,7 +43,6 @@ class TermoraFrame : JFrame(), DataProvider {
private val dataProviderSupport = DataProviderSupport() private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope) private val welcomePanel = WelcomePanel(windowScope)
private val sftp get() = Database.getDatabase().sftp private val sftp get() = Database.getDatabase().sftp
private val myUI = MyFlatRootPaneUI()
private var notifyListeners = emptyArray<NotifyListener>() private var notifyListeners = emptyArray<NotifyListener>()
@@ -88,18 +88,25 @@ class TermoraFrame : JFrame(), DataProvider {
} }
private fun getMouseLayer(): JComponent? { private fun getMouseLayer(): JComponent? {
val titlePane = myUI.getTitlePane() ?: return null val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
handlerField.isAccessible = true handlerField.isAccessible = true
return handlerField.get(titlePane) as? JComponent return handlerField.get(titlePane) as? JComponent
} }
private fun getHandler(): Any? { private fun getHandler(): Any? {
val titlePane = myUI.getTitlePane() ?: return null val titlePane = getTitlePane() ?: return null
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
handlerField.isAccessible = true handlerField.isAccessible = true
return handlerField.get(titlePane) return handlerField.get(titlePane)
} }
private fun getTitlePane(): FlatTitlePane? {
val ui = rootPane.ui as? FlatRootPaneUI ?: return null
val titlePaneField = ui.javaClass.getDeclaredField("titlePane")
titlePaneField.isAccessible = true
return titlePaneField.get(ui) as? FlatTitlePane
}
} }
toolbar.getJToolBar().addMouseListener(mouseAdapter) toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
@@ -173,7 +180,6 @@ class TermoraFrame : JFrame(), DataProvider {
// Windows 10 会有1像素误差 // Windows 10 会有1像素误差
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
} else if (SystemInfo.isLinux) { } else if (SystemInfo.isLinux) {
rootPane.setUI(myUI)
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0) tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
} }
@@ -213,6 +219,11 @@ class TermoraFrame : JFrame(), DataProvider {
} }
} }
val glassPane = GlassPane()
rootPane.glassPane = glassPane
glassPane.isOpaque = false
glassPane.isVisible = true
Disposer.register(windowScope, terminalTabbed) Disposer.register(windowScope, terminalTabbed)
add(terminalTabbed, BorderLayout.CENTER) add(terminalTabbed, BorderLayout.CENTER)
@@ -254,4 +265,19 @@ class TermoraFrame : JFrame(), DataProvider {
super.addNotify() super.addNotify()
notifyListeners.forEach { it.addNotify() } notifyListeners.forEach { it.addNotify() }
} }
private class GlassPane : JComponent() {
override fun paintComponent(g: Graphics) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
val g2d = g as Graphics2D
g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
if (FlatLaf.isLafDark()) 0.2f else 0.1f
)
g2d.drawImage(img, 0, 0, width, height, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
}
}
} }

View File

@@ -55,6 +55,7 @@ termora.settings.appearance.language=Language
termora.settings.appearance.i-want-to-translate=I want to translate termora.settings.appearance.i-want-to-translate=I want to translate
termora.settings.appearance.follow-system=Sync with OS termora.settings.appearance.follow-system=Sync with OS
termora.settings.appearance.opacity=Opacity termora.settings.appearance.opacity=Opacity
termora.settings.appearance.background-image=BG Image
termora.settings.appearance.background-running=Backgrounding termora.settings.appearance.background-running=Backgrounding
termora.setting.security=Security termora.setting.security=Security

View File

@@ -52,6 +52,7 @@ termora.settings.appearance.language=语言
termora.settings.appearance.i-want-to-translate=我想要翻译 termora.settings.appearance.i-want-to-translate=我想要翻译
termora.settings.appearance.follow-system=跟随系统 termora.settings.appearance.follow-system=跟随系统
termora.settings.appearance.opacity=透明度 termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-image=背景图
termora.settings.appearance.background-running=后台运行 termora.settings.appearance.background-running=后台运行
termora.setting.security=安全 termora.setting.security=安全

View File

@@ -53,6 +53,7 @@ termora.settings.appearance.language=語言
termora.settings.appearance.i-want-to-translate=我想要翻譯 termora.settings.appearance.i-want-to-translate=我想要翻譯
termora.settings.appearance.follow-system=跟隨系統 termora.settings.appearance.follow-system=跟隨系統
termora.settings.appearance.opacity=透明度 termora.settings.appearance.opacity=透明度
termora.settings.appearance.background-image=背景圖
termora.settings.appearance.background-running=後台運行 termora.settings.appearance.background-running=後台運行
termora.setting.security=安全 termora.setting.security=安全