feat: support custom layout

This commit is contained in:
hstyi
2025-07-02 12:15:28 +08:00
committed by GitHub
parent ab6b6a2127
commit 9916edbd13
18 changed files with 629 additions and 218 deletions

View File

@@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
size = preferredSize size = preferredSize
} }
override fun paintComponent(g: Graphics) { public override fun paintComponent(g: Graphics) {
if (g is Graphics2D) { if (g is Graphics2D) {
g.setRenderingHints( g.setRenderingHints(
RenderingHints( RenderingHints(
@@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
} }
g.font = font g.font = font
g.color = UIManager.getColor("TextField.placeholderForeground") g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground")
val height = g.fontMetrics.height val height = g.fontMetrics.height
val descent = g.fontMetrics.descent val descent = g.fontMetrics.descent

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.terminal.* import app.termora.terminal.*
@@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
import java.util.*
import javax.swing.Icon import javax.swing.Icon
abstract class HostTerminalTab( abstract class HostTerminalTab(
@@ -20,6 +23,10 @@ abstract class HostTerminalTab(
val Host = DataKey(app.termora.Host::class) 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 coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
protected val terminalModel get() = terminal.getTerminalModel() protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false protected var unread = false
@@ -42,7 +49,10 @@ abstract class HostTerminalTab(
if (hasFocus || unread) { if (hasFocus || unread) {
return return
} }
unread = true // 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
} }
} }
}) })

View File

@@ -2,6 +2,7 @@ package app.termora
object Icons { object Icons {
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") } 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 dbms by lazy { DynamicIcon("icons/dbms.svg", "icons/dbms_dark.svg") }
val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") } val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") }
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") } val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }

View File

@@ -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<Int>,
) : 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()
}
}

View File

@@ -112,6 +112,7 @@ class SettingsOptionsPane : OptionsPane() {
private inner class AppearanceOption : JPanel(BorderLayout()), Option { private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.getInstance() val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>() val themeComboBox = FlatComboBox<String>()
val layoutComboBox = FlatComboBox<TermoraLayout>()
val languageComboBox = FlatComboBox<String>() val languageComboBox = FlatComboBox<String>()
val backgroundComBoBox = YesOrNoComboBox() val backgroundComBoBox = YesOrNoComboBox()
val confirmTabCloseComBoBox = YesOrNoComboBox() val confirmTabCloseComBoBox = YesOrNoComboBox()
@@ -129,6 +130,38 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() { 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 backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows 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 { opacitySpinner.addChangeListener {
val opacity = opacitySpinner.value val opacity = opacitySpinner.value
if (opacity is Double) { if (opacity is Double) {
@@ -307,7 +351,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow", "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() val box = FlatToolBar()
box.add(followSystemCheckBox) box.add(followSystemCheckBox)
@@ -329,6 +373,9 @@ class SettingsOptionsPane : OptionsPane() {
})).xy(5, rows).apply { rows += step } })).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) 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

@@ -32,6 +32,7 @@ class TerminalTabbed(
private val windowScope: WindowScope, private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar, private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane, private val tabbedPane: FlatTabbedPane,
private val layout: TermoraLayout,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider { ) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>() private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener() 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() DynamicExtensionHandler.getInstance()
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension { .register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
@@ -206,11 +193,13 @@ class TerminalTabbed(
// remove ele // remove ele
tabs.removeAt(index) tabs.removeAt(index)
// 新的获取到焦点 if (tabbedPane.tabCount > 0) {
tabs[tabbedPane.selectedIndex].onGrabFocus() // 新的获取到焦点
tabs[tabbedPane.selectedIndex].onGrabFocus()
// 新的真正获取焦点 // 新的真正获取焦点
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow() tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
}
if (disposable) { if (disposable) {
Disposer.dispose(tab) 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() { override fun dispose() {
} }

View File

@@ -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
}
}

View File

@@ -4,21 +4,29 @@ package app.termora
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders 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.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.tree.NewHostTreeModel
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.FlatRootPaneUI
import com.formdev.flatlaf.ui.FlatTitlePane 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 org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import java.awt.* import java.awt.*
import java.awt.event.MouseAdapter import java.awt.event.*
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import java.awt.event.MouseMotionListener
import java.util.* import java.util.*
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.swing.Icon
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JFrame import javax.swing.JFrame
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
@@ -32,15 +40,16 @@ fun assertEventDispatchThread() {
class TermoraFrame : JFrame(), DataProvider { class TermoraFrame : JFrame(), DataProvider {
private val layout get() = TermoraLayout.Layout
private val titleBarHeight = computedTitleBarHeight()
private val id = UUID.randomUUID().toString() private val id = UUID.randomUUID().toString()
private val windowScope = ApplicationScope.forWindowScope(this) 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 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 dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope)
private var notifyListeners = emptyArray<NotifyListener>() private var notifyListeners = emptyArray<NotifyListener>()
private val moveMouseAdapter = createMoveMouseAdaptor()
init { init {
@@ -50,7 +59,209 @@ class TermoraFrame : JFrame(), DataProvider {
private fun initEvents() { private fun initEvents() {
if (SystemInfo.isLinux) { 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<FindEverywhereResult> {
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 "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;<font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
}
}
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 <T : Any> getData(dataKey: DataKey<T>): 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) { override fun mouseClicked(e: MouseEvent) {
getMouseHandler()?.mouseClicked(e) getMouseHandler()?.mouseClicked(e)
} }
@@ -97,8 +308,6 @@ class TermoraFrame : JFrame(), DataProvider {
return titlePaneField.get(ui) as? FlatTitlePane return titlePaneField.get(ui) as? FlatTitlePane
} }
} }
toolbar.getJToolBar().addMouseListener(mouseAdapter)
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
} }
/// force hit /// 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) JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
return mouseAdapter
} }
} }
}
return object : 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 <T : Any> getData(dataKey: DataKey<T>): 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() }
} }

View File

@@ -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
}
}
}

View File

@@ -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<NewHostTree>(welcomePanel.getData(DataProviders.Welcome.HostTree))
}
}

View File

@@ -4,14 +4,9 @@ package app.termora
import app.termora.actions.* import app.termora.actions.*
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import app.termora.findeverywhere.FindEverywhereProvider 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.terminal.DataKey
import app.termora.tree.* import app.termora.tree.*
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatButton import com.formdev.flatlaf.extras.components.FlatButton
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -24,8 +19,7 @@ import java.awt.event.*
import javax.swing.* import javax.swing.*
import kotlin.math.max import kotlin.math.max
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab, class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProvider {
DataProvider {
private val properties get() = DatabaseManager.getInstance().properties private val properties get() = DatabaseManager.getInstance().properties
private val rootPanel = JPanel(BorderLayout()) private val rootPanel = JPanel(BorderLayout())
@@ -52,6 +46,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
panel.add(createSearchPanel(), BorderLayout.NORTH) panel.add(createSearchPanel(), BorderLayout.NORTH)
panel.add(createHostPanel(), BorderLayout.CENTER) panel.add(createHostPanel(), BorderLayout.CENTER)
bannerPanel.foreground = UIManager.getColor("TextField.placeholderForeground")
if (!fullContent) { if (!fullContent) {
rootPanel.add(bannerPanel, BorderLayout.NORTH) 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<FindEverywhereResult> {
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() { private fun perform() {
@@ -302,40 +259,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
properties.putString("WelcomeFullContent", fullContent.toString()) 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 "<html>${host.name}&nbsp;&nbsp;&nbsp;&nbsp;<font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
}
}
return host.name
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey) return dataProviderSupport.getData(dataKey)
} }

View File

@@ -722,6 +722,11 @@ class DatabaseManager private constructor() : Disposable {
*/ */
var theme by StringPropertyDelegate("Light") var theme by StringPropertyDelegate("Light")
/**
* 布局
*/
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
/** /**
* 跟随系统 * 跟随系统
*/ */

View File

@@ -1,7 +1,6 @@
package app.termora.plugin.internal.ssh package app.termora.plugin.internal.ssh
import app.termora.* import app.termora.*
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.actions.TabReconnectAction import app.termora.actions.TabReconnectAction
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor 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.apache.sshd.common.session.SessionListener
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.*
import javax.swing.Icon import javax.swing.Icon
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -47,9 +45,6 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
private var sshClient: SshClient? = null private var sshClient: SshClient? = null
private var sshSession: ClientSession? = null private var sshSession: ClientSession? = null
private var sshChannelShell: ChannelShell? = null private var sshChannelShell: ChannelShell? = null
private val terminalTabbedManager
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
.getData(DataProviders.TerminalTabbedManager)
init { init {
terminalPanel.dropFiles = false terminalPanel.dropFiles = false

View File

@@ -51,6 +51,9 @@ termora.setting=Settings
termora.settings.appearance=General termora.settings.appearance=General
termora.settings.appearance.theme=Theme 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.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

View File

@@ -54,6 +54,9 @@ termora.settings.restart.manually=请手动重启软件
termora.settings.appearance=常规 termora.settings.appearance=常规
termora.settings.appearance.theme=主题 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.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=跟随系统

View File

@@ -53,6 +53,9 @@ termora.settings.restart.manually=請手動重新啟動軟體
termora.settings.appearance=一般 termora.settings.appearance=一般
termora.settings.appearance.theme=主题 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.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=跟隨系統

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4C1 2.89543 1.89543 2 3 2H13C14.1046 2 15 2.89543 15 4V12C15 13.1046 14.1046 14 13 14H3C1.89543 14 1 13.1046 1 12V4ZM3 3H5V13H3C2.44772 13 2 12.5523 2 12V4C2 3.44772 2.44772 3 3 3ZM6 3V13H13C13.5523 13 14 12.5523 14 12V4C14 3.44772 13.5523 3 13 3H6Z" fill="#6C707E"/>
<path d="M2 4C2 3.44772 2.44772 3 3 3H5V13H3C2.44772 13 2 12.5523 2 12V4Z" />
</svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -0,0 +1,12 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5436_53300)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4C1 2.89543 1.89543 2 3 2H13C14.1046 2 15 2.89543 15 4V12C15 13.1046 14.1046 14 13 14H3C1.89543 14 1 13.1046 1 12V4ZM3 3H5V13H3C2.44772 13 2 12.5523 2 12V4C2 3.44772 2.44772 3 3 3ZM6 3V13H13C13.5523 13 14 12.5523 14 12V4C14 3.44772 13.5523 3 13 3H6Z" fill="#CED0D6"/>
<path d="M2 4C2 3.44772 2.44772 3 3 3H5V13H3C2.44772 13 2 12.5523 2 12V4Z" />
</g>
<defs>
<clipPath id="clip0_5436_53300">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 776 B