diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index f9a74c5..ab8a7f0 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -64,6 +64,9 @@ class ApplicationRunner { fileSystemManager.filesCache = WeakRefFilesCache() fileSystemManager.init() VFS.setManager(fileSystemManager) + + // async init + BackgroundManager.getInstance().getBackgroundImage() } // 设置 LAF diff --git a/src/main/kotlin/app/termora/BackgroundManager.kt b/src/main/kotlin/app/termora/BackgroundManager.kt new file mode 100644 index 0000000..11bec50 --- /dev/null +++ b/src/main/kotlin/app/termora/BackgroundManager.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index 2395703..0715404 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -643,6 +643,11 @@ class Database private constructor(private val env: Environment) : Disposable { */ var backgroundRunning by BooleanPropertyDelegate(false) + /** + * 背景图片的地址 + */ + var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY) + /** * 语言 */ diff --git a/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt b/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt deleted file mode 100644 index 93f3981..0000000 --- a/src/main/kotlin/app/termora/MyFlatRootPaneUI.kt +++ /dev/null @@ -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 - } -} \ 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 745470f..a7a51ef 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -39,6 +39,8 @@ import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.withContext import kotlinx.serialization.json.* 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.lang3.StringUtils import org.apache.commons.lang3.SystemUtils @@ -132,8 +134,11 @@ class SettingsOptionsPane : OptionsPane() { val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val preferredThemeBtn = JButton(Icons.settings) val opacitySpinner = NumberSpinner(100, 0, 100) + val backgroundImageTextField = OutlineTextField() private val appearance get() = database.appearance + private val backgroundButton = JButton(Icons.folder) + private val backgroundClearButton = FlatButton() init { initView() @@ -143,6 +148,14 @@ class SettingsOptionsPane : OptionsPane() { private fun initView() { 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.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) { @@ -239,6 +252,45 @@ class SettingsOptionsPane : OptionsPane() { } 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 { @@ -308,7 +360,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, $formMargin, pref" + "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" ) val box = FlatToolBar() box.add(followSystemCheckBox) @@ -330,6 +382,13 @@ class SettingsOptionsPane : OptionsPane() { })).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) .add(opacitySpinner).xy(3, rows).apply { rows += step } diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index 6d423b3..581a687 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -7,12 +7,13 @@ import app.termora.actions.DataProviders import app.termora.sftp.SFTPTab import app.termora.terminal.DataKey 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.jetbrains.JBR import org.apache.commons.lang3.ArrayUtils -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Insets +import java.awt.* import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseListener @@ -42,7 +43,6 @@ class TermoraFrame : JFrame(), DataProvider { private val dataProviderSupport = DataProviderSupport() private val welcomePanel = WelcomePanel(windowScope) private val sftp get() = Database.getDatabase().sftp - private val myUI = MyFlatRootPaneUI() private var notifyListeners = emptyArray() @@ -88,18 +88,25 @@ class TermoraFrame : JFrame(), DataProvider { } private fun getMouseLayer(): JComponent? { - val titlePane = myUI.getTitlePane() ?: return null + val titlePane = getTitlePane() ?: return null val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null handlerField.isAccessible = true return handlerField.get(titlePane) as? JComponent } private fun getHandler(): Any? { - val titlePane = myUI.getTitlePane() ?: return null + val titlePane = getTitlePane() ?: return null val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null handlerField.isAccessible = true 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().addMouseMotionListener(mouseAdapter) @@ -173,7 +180,6 @@ class TermoraFrame : JFrame(), DataProvider { // Windows 10 会有1像素误差 tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) } else if (SystemInfo.isLinux) { - rootPane.setUI(myUI) 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) add(terminalTabbed, BorderLayout.CENTER) @@ -254,4 +265,19 @@ class TermoraFrame : JFrame(), DataProvider { super.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) + } + + } } \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index b2e4d60..61c582a 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -55,6 +55,7 @@ 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-image=BG Image 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 bd0589c..0e442ea 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -52,6 +52,7 @@ termora.settings.appearance.language=语言 termora.settings.appearance.i-want-to-translate=我想要翻译 termora.settings.appearance.follow-system=跟随系统 termora.settings.appearance.opacity=透明度 +termora.settings.appearance.background-image=背景图 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 4236849..999ee32 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -53,6 +53,7 @@ termora.settings.appearance.language=語言 termora.settings.appearance.i-want-to-translate=我想要翻譯 termora.settings.appearance.follow-system=跟隨系統 termora.settings.appearance.opacity=透明度 +termora.settings.appearance.background-image=背景圖 termora.settings.appearance.background-running=後台運行 termora.setting.security=安全