mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support setting background image (#475)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
72
src/main/kotlin/app/termora/BackgroundManager.kt
Normal file
72
src/main/kotlin/app/termora/BackgroundManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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=安全
|
||||||
|
|||||||
@@ -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=安全
|
||||||
|
|||||||
Reference in New Issue
Block a user