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.DataProvider
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.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
@@ -29,11 +31,6 @@ abstract class HostTerminalTab(
.getData(DataProviders.TerminalTabbedManager)
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
protected val terminalModel get() = terminal.getTerminalModel()
protected var unread = false
set(value) {
field = value
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
}
/* visualTerminal */
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
terminal.getTerminalModel().setData(Host, host)
terminal.getTerminalModel().addDataListener(object : DataListener {
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() {
super.onGrabFocus()
if (!unread) return
unread = false
}
@Suppress("UNCHECKED_CAST")

View File

@@ -2,19 +2,20 @@ package app.termora
import app.termora.actions.AnActionEvent
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.ui.FlatTabbedPaneUI
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.*
import java.awt.image.BufferedImage
import java.util.*
import javax.swing.ImageIcon
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.SwingUtilities
import javax.swing.*
import kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
class MyTabbedPane : FlatTabbedPane(), Disposable {
private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager
@@ -23,6 +24,14 @@ class MyTabbedPane : FlatTabbedPane() {
private val owner
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.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 {
isFocusable = false
@@ -38,6 +47,16 @@ class MyTabbedPane : FlatTabbedPane() {
private fun initEvents() {
addMouseListener(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) {
@@ -70,6 +89,29 @@ class MyTabbedPane : FlatTabbedPane() {
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 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 title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(),
c,
StringUtils.EMPTY,
index
)
tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
// 设置标题
c.putClientProperty(titleProperty, title)

View File

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

View File

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

View File

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

View File

@@ -226,7 +226,7 @@ class SSHTerminalTab(
}
override fun getIcon(): Icon {
return if (unread) Icons.terminalUnread else Icons.terminal
return Icons.terminal
}
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. -->
<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>

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. -->
<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>

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 701 B