feat: add tab index

This commit is contained in:
hstyi
2025-08-07 20:53:52 +08:00
committed by hstyi
parent de9b418c75
commit 969ddc3662
9 changed files with 135 additions and 35 deletions

View File

@@ -3,14 +3,16 @@ package app.termora
import app.termora.actions.AnActionEvent 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.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.DataListener
import app.termora.terminal.Terminal
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent
import java.util.* import java.util.*
import javax.swing.Icon import javax.swing.Icon
@@ -29,11 +31,6 @@ abstract class HostTerminalTab(
.getData(DataProviders.TerminalTabbedManager) .getData(DataProviders.TerminalTabbedManager)
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing) protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
protected val terminalModel get() = terminal.getTerminalModel() protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false
set(value) {
field = value
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
}
/* visualTerminal */ /* visualTerminal */
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
terminal.getTerminalModel().setData(Host, host) terminal.getTerminalModel().setData(Host, host)
terminal.getTerminalModel().addDataListener(object : DataListener { terminal.getTerminalModel().addDataListener(object : DataListener {
override fun onChanged(key: DataKey<*>, data: Any) { override fun onChanged(key: DataKey<*>, data: Any) {
if (key == VisualTerminal.Written) {
if (hasFocus || unread) {
return
}
// 如果当前选中的不是这个 Tab那么设置成未读
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
unread = true
}
}
} }
}) })
} }
@@ -75,8 +63,6 @@ abstract class HostTerminalTab(
override fun onGrabFocus() { override fun onGrabFocus() {
super.onGrabFocus() super.onGrabFocus()
if (!unread) return
unread = false
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@@ -2,19 +2,20 @@ package app.termora
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders import app.termora.actions.DataProviders
import app.termora.actions.SwitchTabAction
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.ui.FlatTabbedPaneUI
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.*
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.* import java.util.*
import javax.swing.ImageIcon import javax.swing.*
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.SwingUtilities
import kotlin.math.abs import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() { class MyTabbedPane : FlatTabbedPane(), Disposable {
private val dragMouseAdaptor = DragMouseAdaptor() private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager private val terminalTabbedManager
@@ -23,6 +24,14 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this)) get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TermoraFrame) as TermoraFrame .getData(DataProviders.TermoraFrame) as TermoraFrame
private val keymap get() = KeymapManager.getInstance().getActiveKeymap()
private var isSwitchTabMode = false
set(value) {
field = value
repaint()
}
private val isScreen get() = TermoraLayout.Layout == TermoraLayout.Screen
init { init {
isFocusable = false isFocusable = false
@@ -38,6 +47,16 @@ class MyTabbedPane : FlatTabbedPane() {
private fun initEvents() { private fun initEvents() {
addMouseListener(dragMouseAdaptor) addMouseListener(dragMouseAdaptor)
addMouseMotionListener(dragMouseAdaptor) addMouseMotionListener(dragMouseAdaptor)
val awtEventListener = MyAWTEventListener()
toolkit.addAWTEventListener(awtEventListener, AWTEvent.KEY_EVENT_MASK or AWTEvent.WINDOW_EVENT_MASK)
Disposer.register(this, object : Disposable {
override fun dispose() {
toolkit.removeAWTEventListener(awtEventListener)
}
})
} }
override fun processMouseEvent(e: MouseEvent) { override fun processMouseEvent(e: MouseEvent) {
@@ -70,6 +89,29 @@ class MyTabbedPane : FlatTabbedPane() {
firePropertyChange("selectedIndex", oldIndex, index) firePropertyChange("selectedIndex", oldIndex, index)
} }
override fun updateUI() {
super.updateUI()
setUI(MyMyTabbedPaneUI())
}
private inner class MyAWTEventListener : AWTEventListener {
override fun eventDispatched(event: AWTEvent) {
if (event is KeyEvent) {
if (isSwitchTabMode) isSwitchTabMode = false
val shortcuts = keymap.getShortcut(SwitchTabAction.SWITCH_TAB)
if (shortcuts.isEmpty()) return
val shortcut = shortcuts.first() as KeyShortcut
val modifiers = KeyStroke.getKeyStroke(event.keyCode, event.modifiersEx).modifiers
if (shortcut.keyStroke.modifiers != modifiers) return
if (SwingUtilities.getWindowAncestor(event.component) != owner) return
if (isSwitchTabMode.not()) isSwitchTabMode = true
} else if (event is WindowEvent) {
if (event.id == WindowEvent.WINDOW_LOST_FOCUS || event.id == WindowEvent.WINDOW_DEACTIVATED) {
if (isSwitchTabMode) isSwitchTabMode = false
}
}
}
}
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher { private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
private var mousePressedPoint = Point() private var mousePressedPoint = Point()
@@ -267,5 +309,81 @@ class MyTabbedPane : FlatTabbedPane() {
} }
} }
private inner class MyMyTabbedPaneUI : FlatTabbedPaneUI() {
override fun paintIcon(
g: Graphics,
tabPlacement: Int,
tabIndex: Int,
icon: Icon,
iconRect: Rectangle?,
isSelected: Boolean
) {
super.paintIcon(g, tabPlacement, tabIndex, MyIcon(icon, tabIndex, isSelected), iconRect, isSelected)
}
override fun createMoreTabsButton(): JButton {
return MyMoreTabsButton()
}
private inner class MyMoreTabsButton : FlatMoreTabsButton() {
override fun createTabMenuItem(tabIndex: Int): JMenuItem? {
val item = super.createTabMenuItem(tabIndex)
if (tabIndex == 0 && isScreen) {
item.text = Application.getName()
}
return item
}
}
}
override fun getIconAt(index: Int): Icon? {
if (isSwitchTabMode) {
return MyIcon(super.getIconAt(index), index, selectedIndex == index)
}
return super.getIconAt(index)
}
private inner class MyIcon(private val icon: Icon, private val tabIndex: Int, private val isSelected: Boolean) :
Icon {
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
if (isScreen && tabIndex == 0) {
icon.paintIcon(c, g, x, y)
return
}
if (isSwitchTabMode.not()) {
icon.paintIcon(c, g, x, y)
return
}
if (g !is Graphics2D) return
g.save()
setupAntialiasing(g)
val fm = g.getFontMetrics(g.font)
val text = "${tabIndex + 1}"
val textWidth = fm.stringWidth(text)
val textHeight = fm.ascent
val centerX = x + (icon.iconWidth - textWidth) / 2
val centerY = y + (icon.iconHeight + textHeight) / 2 - 1
g.color = c.getForeground()
g.drawString(text, centerX, centerY)
g.restore()
}
override fun getIconWidth(): Int {
return icon.iconWidth
}
override fun getIconHeight(): Int {
return icon.iconHeight
}
}
} }

View File

@@ -337,13 +337,7 @@ class TerminalTabbed(
val c = tab.getJComponent() val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString() val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab( tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
title,
tab.getIcon(),
c,
StringUtils.EMPTY,
index
)
// 设置标题 // 设置标题
c.putClientProperty(titleProperty, title) c.putClientProperty(titleProperty, title)

View File

@@ -164,6 +164,8 @@ class TermoraFrame : JFrame(), DataProvider {
}).let { Disposer.register(windowScope, it) } }).let { Disposer.register(windowScope, it) }
Disposer.register(windowScope, tabbedPane)
} }
private fun initView() { private fun initView() {

View File

@@ -224,7 +224,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
override fun getTitle(): String { override fun getTitle(): String {
return I18n.getString("termora.title") return StringUtils.EMPTY
} }
override fun getIcon(): Icon { override fun getIcon(): Icon {

View File

@@ -31,7 +31,7 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
} }
override fun getIcon(): Icon { override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal return Icons.terminal
} }
override fun willBeClose(): Boolean { override fun willBeClose(): Boolean {

View File

@@ -226,7 +226,7 @@ class SSHTerminalTab(
} }
override fun getIcon(): Icon { override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal return Icons.terminal
} }
override fun beforeClose() { override fun beforeClose() {

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> <!-- Copyright 2000-2023 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"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#EBECF0" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -1,4 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> <!-- Copyright 2000-2023 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"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" fill="#43454A" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.33214 2.63203L13.3321 7.07539C13.4389 7.17028 13.5 7.3063 13.5 7.44914V13C13.5 13.2761 13.2761 13.5 13 13.5H10C9.72386 13.5 9.5 13.2761 9.5 13V11C9.5 10.1716 8.82843 9.5 8 9.5C7.17157 9.5 6.5 10.1716 6.5 11V13C6.5 13.2761 6.27614 13.5 6 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V7.44914C2.5 7.3063 2.56109 7.17028 2.66786 7.07539L7.66786 2.63203C7.85729 2.46369 8.14271 2.46369 8.33214 2.63203Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B