From 9916edbd136bdbadbc438b364316de54d631f2b4 Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 2 Jul 2025 12:15:28 +0800 Subject: [PATCH] feat: support custom layout --- src/main/kotlin/app/termora/BannerPanel.kt | 4 +- .../kotlin/app/termora/HostTerminalTab.kt | 12 +- src/main/kotlin/app/termora/Icons.kt | 1 + .../termora/JSplitPaneWithZeroSizeDivider.kt | 123 +++++++ .../kotlin/app/termora/SettingsOptionsPane.kt | 49 ++- src/main/kotlin/app/termora/TerminalTabbed.kt | 51 ++- .../kotlin/app/termora/TermoraFencePanel.kt | 107 ++++++ src/main/kotlin/app/termora/TermoraFrame.kt | 335 ++++++++++++------ src/main/kotlin/app/termora/TermoraLayout.kt | 18 + .../kotlin/app/termora/TermoraScreenPanel.kt | 30 ++ src/main/kotlin/app/termora/WelcomePanel.kt | 81 +---- .../app/termora/database/DatabaseManager.kt | 5 + .../plugin/internal/ssh/SSHTerminalTab.kt | 5 - src/main/resources/i18n/messages.properties | 3 + .../resources/i18n/messages_zh_CN.properties | 3 + .../resources/i18n/messages_zh_TW.properties | 3 + src/main/resources/icons/dataColumn.svg | 5 + src/main/resources/icons/dataColumn_dark.svg | 12 + 18 files changed, 629 insertions(+), 218 deletions(-) create mode 100644 src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt create mode 100644 src/main/kotlin/app/termora/TermoraFencePanel.kt create mode 100644 src/main/kotlin/app/termora/TermoraLayout.kt create mode 100644 src/main/kotlin/app/termora/TermoraScreenPanel.kt create mode 100644 src/main/resources/icons/dataColumn.svg create mode 100644 src/main/resources/icons/dataColumn_dark.svg diff --git a/src/main/kotlin/app/termora/BannerPanel.kt b/src/main/kotlin/app/termora/BannerPanel.kt index 627f413..f9684a3 100644 --- a/src/main/kotlin/app/termora/BannerPanel.kt +++ b/src/main/kotlin/app/termora/BannerPanel.kt @@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone size = preferredSize } - override fun paintComponent(g: Graphics) { + public override fun paintComponent(g: Graphics) { if (g is Graphics2D) { g.setRenderingHints( RenderingHints( @@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone } g.font = font - g.color = UIManager.getColor("TextField.placeholderForeground") + g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground") val height = g.fontMetrics.height val descent = g.fontMetrics.descent diff --git a/src/main/kotlin/app/termora/HostTerminalTab.kt b/src/main/kotlin/app/termora/HostTerminalTab.kt index 3613541..c586b7f 100644 --- a/src/main/kotlin/app/termora/HostTerminalTab.kt +++ b/src/main/kotlin/app/termora/HostTerminalTab.kt @@ -1,5 +1,6 @@ package app.termora +import app.termora.actions.AnActionEvent import app.termora.actions.DataProvider import app.termora.actions.DataProviders import app.termora.terminal.* @@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.swing.Swing +import org.apache.commons.lang3.StringUtils import java.beans.PropertyChangeEvent +import java.util.* import javax.swing.Icon abstract class HostTerminalTab( @@ -20,6 +23,10 @@ abstract class HostTerminalTab( val Host = DataKey(app.termora.Host::class) } + + protected val terminalTabbedManager + get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent())) + .getData(DataProviders.TerminalTabbedManager) protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) } protected val terminalModel get() = terminal.getTerminalModel() protected var unread = false @@ -42,7 +49,10 @@ abstract class HostTerminalTab( if (hasFocus || unread) { return } - unread = true + // 如果当前选中的不是这个 Tab,那么设置成未读 + if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) { + unread = true + } } } }) diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index da21634..635ace2 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -2,6 +2,7 @@ package app.termora object Icons { val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") } + val dataColumn by lazy { DynamicIcon("icons/dataColumn.svg", "icons/dataColumn_dark.svg") } val dbms by lazy { DynamicIcon("icons/dbms.svg", "icons/dbms_dark.svg") } val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") } val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") } diff --git a/src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt b/src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt new file mode 100644 index 0000000..eda3825 --- /dev/null +++ b/src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt @@ -0,0 +1,123 @@ +package app.termora + +import com.formdev.flatlaf.ui.FlatSplitPaneUI +import java.awt.BorderLayout +import java.awt.Cursor +import java.awt.Graphics +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionAdapter +import java.util.function.Supplier +import javax.swing.* + +class SplitPaneUI : FlatSplitPaneUI() { + public override fun startDragging() { + super.startDragging() + } + + public override fun dragDividerTo(location: Int) { + super.dragDividerTo(location) + } + + public override fun finishDraggingTo(location: Int) { + super.finishDraggingTo(location) + } +} + +class JSplitPaneWithZeroSizeDivider( + private val splitPane: JSplitPane, + private val topOffset: Supplier, +) : JPanel(BorderLayout()) { + + private val dividerDragSize = 7 + private val layeredPane = LayeredPane() + private val divider = Divider() + + init { + layeredPane.add(splitPane, JLayeredPane.DEFAULT_LAYER as Any) + layeredPane.add(divider, JLayeredPane.PALETTE_LAYER as Any) + add(layeredPane, BorderLayout.CENTER) + } + + private inner class Divider : JComponent() { + private var dragging = false + private var dragStartX = 0 + private var initialDividerLocation = 0 + private val splitPaneUI get() = splitPane.ui as SplitPaneUI + + init { + cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR) + addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + if (SwingUtilities.isLeftMouseButton(e)) { + dragging = true + dragStartX = e.xOnScreen + initialDividerLocation = splitPane.dividerLocation + splitPaneUI.startDragging() + } + } + + override fun mouseReleased(e: MouseEvent) { + if (SwingUtilities.isLeftMouseButton(e)) { + if (dragging) { + val deltaX = e.xOnScreen - dragStartX + val newLocation = initialDividerLocation + deltaX + splitPaneUI.finishDraggingTo(newLocation) + } + dragging = false + } + } + }) + + addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseDragged(e: MouseEvent) { + if (dragging) { + val deltaX = e.xOnScreen - dragStartX + val newLocation = initialDividerLocation + deltaX + splitPaneUI.dragDividerTo(newLocation) + } + } + }) + } + + override fun paint(g: Graphics) { + g.color = UIManager.getColor("controlShadow") + g.fillRect(width / 2, 0, 1, height) + } + } + + + private inner class LayeredPane : JLayeredPane() { + private val w get() = (dividerDragSize - 1) / 2 + + override fun doLayout() { + synchronized(treeLock) { + for (c in components) { + if (c == divider) { + c.setBounds( + splitPane.dividerLocation - w, + topOffset.get(), + dividerDragSize, + height - topOffset.get() + ) + } else { + c.setBounds(0, 0, width, height) + } + } + } + } + + override fun paint(g: Graphics) { + super.paint(g) + g.color = UIManager.getColor("controlShadow") + g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get()) + } + } + + override fun doLayout() { + super.doLayout() + layeredPane.doLayout() + } + + +} \ 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 1223f17..97f2436 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -112,6 +112,7 @@ class SettingsOptionsPane : OptionsPane() { private inner class AppearanceOption : JPanel(BorderLayout()), Option { val themeManager = ThemeManager.getInstance() val themeComboBox = FlatComboBox() + val layoutComboBox = FlatComboBox() val languageComboBox = FlatComboBox() val backgroundComBoBox = YesOrNoComboBox() val confirmTabCloseComBoBox = YesOrNoComboBox() @@ -129,6 +130,38 @@ class SettingsOptionsPane : OptionsPane() { private fun initView() { + layoutComboBox.addItem(TermoraLayout.Screen) + layoutComboBox.addItem(TermoraLayout.Fence) + layoutComboBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + var text = value + if (value == TermoraLayout.Screen) { + text = I18n.getString("termora.settings.appearance.layout.screen") + } else if (value == TermoraLayout.Fence) { + text = I18n.getString("termora.settings.appearance.layout.fence") + } + + val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus) + icon = null + + if (value == TermoraLayout.Screen) { + icon = if (isSelected) Icons.uiForm.dark else Icons.uiForm + } else if (value == TermoraLayout.Fence) { + icon = if (isSelected) Icons.dataColumn.dark else Icons.dataColumn + } + + return c + } + } + layoutComboBox.selectedItem = runCatching { TermoraLayout.valueOf(appearance.layout) } + .getOrNull() ?: TermoraLayout.Layout + backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows @@ -184,6 +217,17 @@ class SettingsOptionsPane : OptionsPane() { } } + layoutComboBox.addItemListener(object : ItemListener { + override fun itemStateChanged(e: ItemEvent) { + if (e.stateChange == ItemEvent.SELECTED) { + appearance.layout = layoutComboBox.selectedItem?.toString() ?: return + if (TermoraLayout.Layout.name != appearance.layout) { + SwingUtilities.invokeLater { TermoraRestarter.getInstance().scheduleRestart(owner) } + } + } + } + }) + opacitySpinner.addChangeListener { val opacity = opacitySpinner.value if (opacity is Double) { @@ -307,7 +351,7 @@ class SettingsOptionsPane : OptionsPane() { private fun getFormPanel(): JPanel { val layout = FormLayout( "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow", - "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" ) val box = FlatToolBar() box.add(followSystemCheckBox) @@ -329,6 +373,9 @@ class SettingsOptionsPane : OptionsPane() { })).xy(5, rows).apply { rows += step } + builder.add("${I18n.getString("termora.settings.appearance.layout")}:").xy(1, rows) + .add(layoutComboBox).xy(3, 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/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index 920bf6a..8f9267f 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -32,6 +32,7 @@ class TerminalTabbed( private val windowScope: WindowScope, private val termoraToolBar: TermoraToolBar, private val tabbedPane: FlatTabbedPane, + private val layout: TermoraLayout, ) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider { private val tabs = mutableListOf() private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener() @@ -110,20 +111,6 @@ class TerminalTabbed( } }) - - // 点击 - tabbedPane.addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - if (SwingUtilities.isLeftMouseButton(e)) { - val index = tabbedPane.indexAtLocation(e.x, e.y) - if (index > 0) { - tabbedPane.getComponentAt(index).requestFocusInWindow() - } - } - } - }) - - // 注册全局搜索 DynamicExtensionHandler.getInstance() .register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension { @@ -206,11 +193,13 @@ class TerminalTabbed( // remove ele tabs.removeAt(index) - // 新的获取到焦点 - tabs[tabbedPane.selectedIndex].onGrabFocus() + if (tabbedPane.tabCount > 0) { + // 新的获取到焦点 + tabs[tabbedPane.selectedIndex].onGrabFocus() - // 新的真正获取焦点 - tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow() + // 新的真正获取焦点 + tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow() + } if (disposable) { Disposer.dispose(tab) @@ -497,6 +486,32 @@ class TerminalTabbed( } } + override fun paint(g: Graphics) { + super.paint(g) + } + + private val border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor) + private val banner = BannerPanel(fontSize = 13).apply { + foreground = UIManager.getColor("textInactiveText") + } + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + + if (layout == TermoraLayout.Fence) { + if (g is Graphics2D) { + if (tabbedPane.tabCount < 1) { + border.paintBorder(this, g, 0, tabbedPane.tabHeight, width, tabbedPane.tabHeight) + banner.setBounds(0, 0, width, height) + g.save() + g.translate(0, 180) + banner.paintComponent(g) + g.restore() + } + } + } + } + override fun dispose() { } diff --git a/src/main/kotlin/app/termora/TermoraFencePanel.kt b/src/main/kotlin/app/termora/TermoraFencePanel.kt new file mode 100644 index 0000000..d6254e7 --- /dev/null +++ b/src/main/kotlin/app/termora/TermoraFencePanel.kt @@ -0,0 +1,107 @@ +package app.termora + +import app.termora.tree.NewHostTree +import com.formdev.flatlaf.extras.components.FlatTabbedPane +import com.formdev.flatlaf.util.SystemInfo +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import java.awt.event.MouseAdapter +import javax.swing.* + + +class TermoraFencePanel( + private val terminalTabbed: TerminalTabbed, + private val tabbed: FlatTabbedPane, + private val moveMouseAdapter: MouseAdapter, +) : JPanel(BorderLayout()), Disposable { + private val splitPane = object : JSplitPane() { + override fun updateUI() { + setUI(SplitPaneUI()) + revalidate() + } + } + private val leftTreePanel = LeftTreePanel() + private val mySplitPane = JSplitPaneWithZeroSizeDivider(splitPane) { tabbed.tabHeight } + private val enableManager get() = EnableManager.getInstance() + + init { + initView() + initEvents() + } + + private fun initView() { + + splitPane.border = null + + splitPane.leftComponent = leftTreePanel + splitPane.rightComponent = terminalTabbed + splitPane.dividerSize = 1 + splitPane.isEnabled = false + splitPane.dividerLocation = enableManager.getFlag("Termora.Fence.dividerLocation", 220) + + leftTreePanel.preferredSize = Dimension(180, -1) + + tabbed.tabType = FlatTabbedPane.TabType.underlined + tabbed.tabAreaInsets = null + + add(mySplitPane, BorderLayout.CENTER) + } + + private fun initEvents() { + Disposer.register(this, leftTreePanel) + splitPane.addPropertyChangeListener("dividerLocation") { mySplitPane.doLayout() } + } + + private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable { + val hostTree = NewHostTree() + private val box = JToolBar().apply { isFloatable = false } + + init { + initView() + initEvents() + } + + private fun initView() { + val scrollPane = JScrollPane(hostTree) + hostTree.name = "FenceHostTree" + hostTree.restoreExpansions() + box.preferredSize = Dimension(-1, tabbed.tabHeight) + + val label = JLabel(Application.getName()) + label.foreground = UIManager.getColor("textInactiveText") + label.font = label.font.deriveFont(Font.BOLD) + box.add(Box.createHorizontalGlue()) + if (SystemInfo.isMacOS.not()) box.add(label) + box.add(Box.createHorizontalGlue()) + + if (SystemInfo.isMacOS || SystemInfo.isLinux) { + box.addMouseListener(moveMouseAdapter) + box.addMouseMotionListener(moveMouseAdapter) + } + + scrollPane.verticalScrollBar.unitIncrement = 16 + scrollPane.horizontalScrollBar.unitIncrement = 16 + scrollPane.border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor), + BorderFactory.createEmptyBorder(4, 4, 0, 4) + ) + + add(box, BorderLayout.NORTH) + add(scrollPane, BorderLayout.CENTER) + } + + private fun initEvents() { + Disposer.register(this, hostTree) + } + } + + override fun dispose() { + enableManager.setFlag("Termora.Fence.dividerLocation", splitPane.dividerLocation) + } + + fun getHostTree(): NewHostTree { + return leftTreePanel.hostTree + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index 9dcf951..c91924a 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -4,21 +4,29 @@ package app.termora import app.termora.actions.DataProvider import app.termora.actions.DataProviderSupport import app.termora.actions.DataProviders +import app.termora.actions.OpenHostAction +import app.termora.findeverywhere.FindEverywhereProvider +import app.termora.findeverywhere.FindEverywhereProviderExtension +import app.termora.findeverywhere.FindEverywhereResult import app.termora.plugin.ExtensionManager +import app.termora.plugin.internal.extension.DynamicExtensionHandler +import app.termora.plugin.internal.ssh.SSHProtocolProvider import app.termora.terminal.DataKey +import app.termora.tree.NewHostTreeModel 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 org.apache.commons.lang3.StringUtils +import org.jdesktop.swingx.action.ActionManager import java.awt.* -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.event.MouseListener -import java.awt.event.MouseMotionListener +import java.awt.event.* import java.util.* import javax.imageio.ImageIO +import javax.swing.Icon import javax.swing.JComponent import javax.swing.JFrame import javax.swing.SwingUtilities.isEventDispatchThread @@ -32,15 +40,16 @@ fun assertEventDispatchThread() { class TermoraFrame : JFrame(), DataProvider { - + private val layout get() = TermoraLayout.Layout + private val titleBarHeight = computedTitleBarHeight() private val id = UUID.randomUUID().toString() private val windowScope = ApplicationScope.forWindowScope(this) - private val tabbedPane = MyTabbedPane() + private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight } private val toolbar = TermoraToolBar(windowScope, this) - private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane) + private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout) private val dataProviderSupport = DataProviderSupport() - private val welcomePanel = WelcomePanel(windowScope) private var notifyListeners = emptyArray() + private val moveMouseAdapter = createMoveMouseAdaptor() init { @@ -50,7 +59,209 @@ class TermoraFrame : JFrame(), DataProvider { private fun initEvents() { if (SystemInfo.isLinux) { - val mouseAdapter = object : MouseAdapter() { + toolbar.getJToolBar().addMouseListener(moveMouseAdapter) + toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter) + } else if (SystemInfo.isMacOS) { + terminalTabbed.addMouseListener(moveMouseAdapter) + terminalTabbed.addMouseMotionListener(moveMouseAdapter) + + tabbedPane.addMouseListener(moveMouseAdapter) + tabbedPane.addMouseMotionListener(moveMouseAdapter) + + toolbar.getJToolBar().addMouseListener(moveMouseAdapter) + toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter) + } + + // FindEverywhere + DynamicExtensionHandler.getInstance() + .register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension { + private val hostTreeModel get() = NewHostTreeModel.getInstance() + + private val provider = object : FindEverywhereProvider { + override fun find(pattern: String, scope: Scope): List { + if (scope != windowScope) return emptyList() + + var filter = hostTreeModel.root.getAllChildren() + .map { it.host } + .filter { it.isFolder.not() } + + if (pattern.isNotBlank()) { + filter = filter.filter { + if (it.protocol == SSHProtocolProvider.PROTOCOL) { + it.name.contains(pattern, true) || it.host.contains(pattern, true) + } else { + it.name.contains(pattern, true) + } + } + } + + return filter.map { HostFindEverywhereResult(it) } + } + + override fun group(): String { + return I18n.getString("termora.find-everywhere.groups.open-new-hosts") + } + + override fun order(): Int { + return Integer.MIN_VALUE + 2 + } + } + + override fun getFindEverywhereProvider(): FindEverywhereProvider { + return provider + } + + private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult { + private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo() + + override fun actionPerformed(e: ActionEvent) { + ActionManager.getInstance() + .getAction(OpenHostAction.OPEN_HOST) + ?.actionPerformed(OpenHostActionEvent(e.source, host, e)) + } + + override fun getIcon(isSelected: Boolean): Icon { + if (isSelected) { + if (!FlatLaf.isLafDark()) { + return Icons.terminal.dark + } + } + return Icons.terminal + } + + override fun getText(isSelected: Boolean): String { + if (showMoreInfo) { + val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText") + val moreInfo = when (host.protocol) { + SSHProtocolProvider.PROTOCOL -> "${host.username}@${host.host}" + "Serial" -> host.options.serialComm.port + else -> StringUtils.EMPTY + } + if (moreInfo.isNotBlank()) { + return "${host.name}    ${moreInfo}" + } + } + return host.name + } + } + + }).let { Disposer.register(windowScope, it) } + + } + + + private fun initView() { + + // macOS 要避开左边的控制栏 + if (SystemInfo.isMacOS) { + tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0) + } else if (SystemInfo.isWindows) { + // Windows 10 会有1像素误差 + tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) + } else if (SystemInfo.isLinux) { + tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0) + } + + val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top + + if (SystemInfo.isWindows || SystemInfo.isLinux) { + rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false) + rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height) + } else if (SystemInfo.isMacOS) { + rootPane.putClientProperty("apple.awt.windowTitleVisible", false) + rootPane.putClientProperty("apple.awt.fullWindowContent", true) + rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + rootPane.putClientProperty( + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING, + FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM + ) + } + + if (SystemInfo.isWindows || SystemInfo.isLinux) { + val sizes = listOf(16, 20, 24, 28, 32, 48, 64, 128) + val loader = TermoraFrame::class.java.classLoader + val images = sizes.mapNotNull { e -> + loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) } + } + iconImages = images + } + + minimumSize = Dimension(640, 400) + + val glassPane = GlassPane() + rootPane.glassPane = glassPane + glassPane.isOpaque = false + glassPane.isVisible = true + + for (extension in ExtensionManager.getInstance().getExtensions(GlassPaneAwareExtension::class.java)) { + extension.setGlassPane(this, glassPane) + } + + if (layout == TermoraLayout.Fence) { + val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter) + add(fencePanel, BorderLayout.CENTER) + dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree()) + Disposer.register(windowScope, fencePanel) + } else { + val screenPanel = TermoraScreenPanel(windowScope, terminalTabbed) + add(screenPanel, BorderLayout.CENTER) + dataProviderSupport.addData(DataProviders.Welcome.HostTree, screenPanel.getHostTree()) + } + + Disposer.register(windowScope, terminalTabbed) + + dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane) + dataProviderSupport.addData(DataProviders.TermoraFrame, this) + dataProviderSupport.addData(DataProviders.WindowScope, windowScope) + } + + override fun getData(dataKey: DataKey): T? { + return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey) + } + + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TermoraFrame + + return id == other.id + } + + override fun hashCode(): Int { + 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() } + } + + private fun computedTitleBarHeight(): Int { + val tabHeight = UIManager.getInt("TabbedPane.tabHeight") + if (SystemInfo.isWindows) { + // Windows 10 会有1像素误差 + return tabHeight + if (SystemInfo.isWindows_11_orLater) 1 else 2 + } else if (SystemInfo.isLinux) { + return tabHeight + 1 + } + return tabHeight + } + + private fun createMoveMouseAdaptor(): MouseAdapter { + if (SystemInfo.isLinux) { + return object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { getMouseHandler()?.mouseClicked(e) } @@ -97,8 +308,6 @@ class TermoraFrame : JFrame(), DataProvider { return titlePaneField.get(ui) as? FlatTitlePane } } - toolbar.getJToolBar().addMouseListener(mouseAdapter) - toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) } /// force hit @@ -145,111 +354,13 @@ class TermoraFrame : JFrame(), DataProvider { } } - terminalTabbed.addMouseListener(mouseAdapter) - terminalTabbed.addMouseMotionListener(mouseAdapter) - - tabbedPane.addMouseListener(mouseAdapter) - tabbedPane.addMouseMotionListener(mouseAdapter) - - toolbar.getJToolBar().addMouseListener(mouseAdapter) - toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) - JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar) + + return mouseAdapter } } - } - - private fun initView() { - - // macOS 要避开左边的控制栏 - if (SystemInfo.isMacOS) { - tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0) - } else if (SystemInfo.isWindows) { - // Windows 10 会有1像素误差 - tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0) - } else if (SystemInfo.isLinux) { - tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0) - } - - val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top - - if (SystemInfo.isWindows || SystemInfo.isLinux) { - rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true) - rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false) - rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false) - rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height) - } else if (SystemInfo.isMacOS) { - rootPane.putClientProperty("apple.awt.windowTitleVisible", false) - rootPane.putClientProperty("apple.awt.fullWindowContent", true) - rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - rootPane.putClientProperty( - FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING, - FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM - ) - } - - if (SystemInfo.isWindows || SystemInfo.isLinux) { - val sizes = listOf(16, 20, 24, 28, 32, 48, 64, 128) - val loader = TermoraFrame::class.java.classLoader - val images = sizes.mapNotNull { e -> - loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) } - } - iconImages = images - } - - minimumSize = Dimension(640, 400) - terminalTabbed.addTerminalTab(welcomePanel) - - val glassPane = GlassPane() - rootPane.glassPane = glassPane - glassPane.isOpaque = false - glassPane.isVisible = true - - for (extension in ExtensionManager.getInstance().getExtensions(GlassPaneAwareExtension::class.java)) { - extension.setGlassPane(this, glassPane) - } - - - Disposer.register(windowScope, terminalTabbed) - add(terminalTabbed, BorderLayout.CENTER) - - dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane) - dataProviderSupport.addData(DataProviders.TermoraFrame, this) - dataProviderSupport.addData(DataProviders.WindowScope, windowScope) - } - - override fun getData(dataKey: DataKey): T? { - return dataProviderSupport.getData(dataKey) - ?: terminalTabbed.getData(dataKey) - ?: welcomePanel.getData(dataKey) - } - - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TermoraFrame - - return id == other.id - } - - override fun hashCode(): Int { - 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() } + return object : MouseAdapter() {} } diff --git a/src/main/kotlin/app/termora/TermoraLayout.kt b/src/main/kotlin/app/termora/TermoraLayout.kt new file mode 100644 index 0000000..0e2e582 --- /dev/null +++ b/src/main/kotlin/app/termora/TermoraLayout.kt @@ -0,0 +1,18 @@ +package app.termora + +import app.termora.database.DatabaseManager + +enum class TermoraLayout { + /** + * Split + */ + Fence, + + Screen, ; + + companion object { + val Layout by lazy { + runCatching { TermoraLayout.valueOf(DatabaseManager.getInstance().appearance.layout) }.getOrNull() ?: Screen + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraScreenPanel.kt b/src/main/kotlin/app/termora/TermoraScreenPanel.kt new file mode 100644 index 0000000..c06040b --- /dev/null +++ b/src/main/kotlin/app/termora/TermoraScreenPanel.kt @@ -0,0 +1,30 @@ +package app.termora + +import app.termora.actions.DataProviders +import app.termora.tree.NewHostTree +import java.awt.BorderLayout +import java.util.* +import javax.swing.JPanel + +class TermoraScreenPanel(private val windowScope: WindowScope, private val terminalTabbed: TerminalTabbed) : + JPanel(BorderLayout()) { + private val welcomePanel = WelcomePanel() + + init { + initView() + initEvents() + } + + private fun initView() { + add(terminalTabbed, BorderLayout.CENTER) + terminalTabbed.addTerminalTab(welcomePanel, true) + } + + private fun initEvents() { + Disposer.register(windowScope, welcomePanel) + } + + fun getHostTree(): NewHostTree { + return Objects.requireNonNull(welcomePanel.getData(DataProviders.Welcome.HostTree)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/WelcomePanel.kt b/src/main/kotlin/app/termora/WelcomePanel.kt index 5b670fe..c5f4b20 100644 --- a/src/main/kotlin/app/termora/WelcomePanel.kt +++ b/src/main/kotlin/app/termora/WelcomePanel.kt @@ -4,14 +4,9 @@ package app.termora import app.termora.actions.* import app.termora.database.DatabaseManager import app.termora.findeverywhere.FindEverywhereProvider -import app.termora.findeverywhere.FindEverywhereProviderExtension -import app.termora.findeverywhere.FindEverywhereResult -import app.termora.plugin.internal.extension.DynamicExtensionHandler -import app.termora.plugin.internal.ssh.SSHProtocolProvider import app.termora.terminal.DataKey import app.termora.tree.* import com.formdev.flatlaf.FlatClientProperties -import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.components.FlatButton import org.apache.commons.lang3.StringUtils @@ -24,8 +19,7 @@ import java.awt.event.* import javax.swing.* import kotlin.math.max -class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab, - DataProvider { +class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProvider { private val properties get() = DatabaseManager.getInstance().properties private val rootPanel = JPanel(BorderLayout()) @@ -52,6 +46,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() val panel = JPanel(BorderLayout()) panel.add(createSearchPanel(), BorderLayout.NORTH) panel.add(createHostPanel(), BorderLayout.CENTER) + bannerPanel.foreground = UIManager.getColor("TextField.placeholderForeground") if (!fullContent) { rootPanel.add(bannerPanel, BorderLayout.NORTH) @@ -209,44 +204,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() } }) - DynamicExtensionHandler.getInstance() - .register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension { - private val provider = object : FindEverywhereProvider { - override fun find(pattern: String, scope: Scope): List { - if (scope != windowScope) return emptyList() - - var filter = hostTreeModel.root.getAllChildren() - .map { it.host } - .filter { it.isFolder.not() } - - if (pattern.isNotBlank()) { - filter = filter.filter { - if (it.protocol == SSHProtocolProvider.PROTOCOL) { - it.name.contains(pattern, true) || it.host.contains(pattern, true) - } else { - it.name.contains(pattern, true) - } - } - } - - return filter.map { HostFindEverywhereResult(it) } - } - - override fun group(): String { - return I18n.getString("termora.find-everywhere.groups.open-new-hosts") - } - - override fun order(): Int { - return Integer.MIN_VALUE + 2 - } - } - - override fun getFindEverywhereProvider(): FindEverywhereProvider { - return provider - } - - }).let { Disposer.register(this, it) } - } private fun perform() { @@ -302,40 +259,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout() properties.putString("WelcomeFullContent", fullContent.toString()) } - private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult { - private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo() - - override fun actionPerformed(e: ActionEvent) { - ActionManager.getInstance() - .getAction(OpenHostAction.OPEN_HOST) - ?.actionPerformed(OpenHostActionEvent(e.source, host, e)) - } - - override fun getIcon(isSelected: Boolean): Icon { - if (isSelected) { - if (!FlatLaf.isLafDark()) { - return Icons.terminal.dark - } - } - return Icons.terminal - } - - override fun getText(isSelected: Boolean): String { - if (showMoreInfo) { - val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText") - val moreInfo = when (host.protocol) { - SSHProtocolProvider.PROTOCOL -> "${host.username}@${host.host}" - "Serial" -> host.options.serialComm.port - else -> StringUtils.EMPTY - } - if (moreInfo.isNotBlank()) { - return "${host.name}    ${moreInfo}" - } - } - return host.name - } - } - override fun getData(dataKey: DataKey): T? { return dataProviderSupport.getData(dataKey) } diff --git a/src/main/kotlin/app/termora/database/DatabaseManager.kt b/src/main/kotlin/app/termora/database/DatabaseManager.kt index 10a4e92..04cb781 100644 --- a/src/main/kotlin/app/termora/database/DatabaseManager.kt +++ b/src/main/kotlin/app/termora/database/DatabaseManager.kt @@ -722,6 +722,11 @@ class DatabaseManager private constructor() : Disposable { */ var theme by StringPropertyDelegate("Light") + /** + * 布局 + */ + var layout by StringPropertyDelegate(TermoraLayout.Screen.name) + /** * 跟随系统 */ diff --git a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHTerminalTab.kt b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHTerminalTab.kt index cb45836..bcc1583 100644 --- a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHTerminalTab.kt @@ -1,7 +1,6 @@ package app.termora.plugin.internal.ssh import app.termora.* -import app.termora.actions.AnActionEvent import app.termora.actions.DataProviders import app.termora.actions.TabReconnectAction import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor @@ -28,7 +27,6 @@ import org.apache.sshd.common.session.Session import org.apache.sshd.common.session.SessionListener import org.slf4j.LoggerFactory import java.nio.charset.StandardCharsets -import java.util.* import javax.swing.Icon import javax.swing.JComponent import javax.swing.SwingUtilities @@ -47,9 +45,6 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : private var sshClient: SshClient? = null private var sshSession: ClientSession? = null private var sshChannelShell: ChannelShell? = null - private val terminalTabbedManager - get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent())) - .getData(DataProviders.TerminalTabbedManager) init { terminalPanel.dropFiles = false diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index b050484..0c5ed44 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -51,6 +51,9 @@ termora.setting=Settings termora.settings.appearance=General termora.settings.appearance.theme=Theme +termora.settings.appearance.layout=Layout +termora.settings.appearance.layout.screen=Screen +termora.settings.appearance.layout.fence=Split termora.settings.appearance.language=Language termora.settings.appearance.i-want-to-translate=I want to translate termora.settings.appearance.follow-system=Sync with OS diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index f315c2f..e9e262a 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -54,6 +54,9 @@ termora.settings.restart.manually=请手动重启软件 termora.settings.appearance=常规 termora.settings.appearance.theme=主题 +termora.settings.appearance.layout=布局 +termora.settings.appearance.layout.screen=全屏 +termora.settings.appearance.layout.fence=分割 termora.settings.appearance.language=语言 termora.settings.appearance.i-want-to-translate=我想要翻译 termora.settings.appearance.follow-system=跟随系统 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index e04a5d3..5119f9e 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -53,6 +53,9 @@ termora.settings.restart.manually=請手動重新啟動軟體 termora.settings.appearance=一般 termora.settings.appearance.theme=主题 +termora.settings.appearance.layout=佈局 +termora.settings.appearance.layout.screen=全螢幕 +termora.settings.appearance.layout.fence=分割 termora.settings.appearance.language=語言 termora.settings.appearance.i-want-to-translate=我想要翻譯 termora.settings.appearance.follow-system=跟隨系統 diff --git a/src/main/resources/icons/dataColumn.svg b/src/main/resources/icons/dataColumn.svg new file mode 100644 index 0000000..1c9ceb4 --- /dev/null +++ b/src/main/resources/icons/dataColumn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataColumn_dark.svg b/src/main/resources/icons/dataColumn_dark.svg new file mode 100644 index 0000000..21e4923 --- /dev/null +++ b/src/main/resources/icons/dataColumn_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + +