feat: support drag and drop sorting

This commit is contained in:
hstyi
2025-01-23 16:12:20 +08:00
committed by hstyi
parent 7964950149
commit df2e9b0743
4 changed files with 181 additions and 19 deletions

View File

@@ -1,12 +1,162 @@
package app.termora
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import com.formdev.flatlaf.extras.components.FlatTabbedPane
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 kotlin.math.abs
class MyTabbedPane : FlatTabbedPane() {
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
private val dragMouseAdaptor = DragMouseAdaptor()
private val terminalTabbedManager
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
.getData(DataProviders.TerminalTabbedManager)
init {
initEvents()
}
private fun initEvents() {
addMouseListener(dragMouseAdaptor)
addMouseMotionListener(dragMouseAdaptor)
}
override fun setSelectedIndex(index: Int) {
val oldIndex = selectedIndex
super.setSelectedIndex(index)
firePropertyChange("selectedIndex", oldIndex, index)
}
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
private var mousePressedPoint = Point()
private var tabIndex = 0 - 1
private var cancelled = false
private var window: Window? = null
private var terminalTab: TerminalTab? = null
private var isDragging = false
private var lastVisitTabIndex = -1
override fun mousePressed(e: MouseEvent) {
val index = indexAtLocation(e.x, e.y)
if (index < 0 || !isTabClosable(index)) {
return
}
tabIndex = index
mousePressedPoint = e.point
}
override fun mouseDragged(e: MouseEvent) {
// 如果正在拖拽中,那么修改 Window 的位置
if (isDragging) {
window?.location = e.locationOnScreen
lastVisitTabIndex = indexAtLocation(e.x, e.y)
} else if (tabIndex >= 0) { // 这里之所以判断是确保在 mousePressed 时已经确定了 Tab
// 有的时候会太灵敏,这里容错一下
val diff = 5
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
startDrag(e)
}
}
}
private fun startDrag(e: MouseEvent) {
if (isDragging) return
val terminalTabbedManager = terminalTabbedManager ?: return
val window = JDialog(owner).also { this.window = it }
window.isUndecorated = true
val image = createTabImage(tabIndex)
window.size = Dimension(image.width, image.height)
window.add(JLabel(ImageIcon(image)))
window.location = e.locationOnScreen
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.removeKeyEventDispatcher(this@DragMouseAdaptor)
}
override fun windowOpened(e: WindowEvent) {
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addKeyEventDispatcher(this@DragMouseAdaptor)
}
})
// 暂时关闭 Tab
terminalTabbedManager.closeTerminalTab(terminalTabbedManager.getTerminalTabs()[tabIndex].also {
terminalTab = it
}, false)
window.isVisible = true
isDragging = true
cancelled = false
}
private fun stopDrag() {
if (!isDragging) {
return
}
val tab = this.terminalTab
val terminalTabbedManager = terminalTabbedManager
if (tab != null && terminalTabbedManager != null) {
// 如果是手动取消
if (cancelled) {
terminalTabbedManager.addTerminalTab(tabIndex, tab)
} else if (lastVisitTabIndex > 0) {
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
} else if (lastVisitTabIndex == 0) {
terminalTabbedManager.addTerminalTab(1, tab)
} else {
terminalTabbedManager.addTerminalTab(tab)
}
}
// reset
window?.dispose()
isDragging = false
tabIndex = -1
cancelled = false
lastVisitTabIndex = -1
}
override fun mouseReleased(e: MouseEvent) {
stopDrag()
}
private fun createTabImage(index: Int): BufferedImage {
val tabBounds = getBoundsAt(index)
val image = BufferedImage(tabBounds.width, tabBounds.height, BufferedImage.TYPE_INT_ARGB)
val g2 = image.createGraphics()
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
g2.translate(-tabBounds.x, -tabBounds.y)
paint(g2)
g2.dispose()
return image
}
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_ESCAPE) {
cancelled = true
stopDrag()
return true
}
return false
}
}
}

View File

@@ -10,12 +10,14 @@ import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.apache.commons.lang3.StringUtils
import java.awt.*
import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.beans.PropertyChangeListener
import java.util.*
import javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min
@@ -30,7 +32,7 @@ class TerminalTabbed(
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val titleProperty = UUID.randomUUID().toSimpleString()
private val iconListener = PropertyChangeListener { e ->
val source = e.source
if (e.propertyName == "icon" && source is TerminalTab) {
@@ -190,16 +192,16 @@ class TerminalTabbed(
// 修改名称
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
rename.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
if (tabIndex > 0) {
val dialog = InputDialog(
SwingUtilities.getWindowAncestor(this),
title = rename.text,
text = tabbedPane.getTitleAt(index),
text = tabbedPane.getTitleAt(tabIndex),
)
val text = dialog.getText()
if (!text.isNullOrBlank()) {
tabbedPane.setTitleAt(index, text)
tabbedPane.setTitleAt(tabIndex, text)
c.putClientProperty(titleProperty, text)
}
}
@@ -276,9 +278,8 @@ class TerminalTabbed(
popupMenu.addSeparator()
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
reconnect.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
tabs[index].reconnect()
if (tabIndex > 0) {
tabs[tabIndex].reconnect()
}
}
@@ -289,18 +290,24 @@ class TerminalTabbed(
}
fun addTab(tab: TerminalTab) {
tabbedPane.addTab(
tab.getTitle(),
private fun addTab(index: Int, tab: TerminalTab) {
val c = tab.getJComponent()
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
tabbedPane.insertTab(
title,
tab.getIcon(),
tab.getJComponent()
c,
StringUtils.EMPTY,
index
)
c.putClientProperty(titleProperty, title)
// 监听 icons 变化
tab.addPropertyChangeListener(iconListener)
tabs.add(tab)
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
tabs.add(index, tab)
tabbedPane.selectedIndex = index
Disposer.register(this, tab)
}
@@ -393,7 +400,11 @@ class TerminalTabbed(
}
override fun addTerminalTab(tab: TerminalTab) {
addTab(tab)
addTab(tabs.size, tab)
}
override fun addTerminalTab(index: Int, tab: TerminalTab) {
addTab(index, tab)
}
override fun getSelectedTerminalTab(): TerminalTab? {
@@ -418,10 +429,10 @@ class TerminalTabbed(
}
}
override fun closeTerminalTab(tab: TerminalTab) {
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, true)
removeTabAt(i, disposable)
break
}
}

View File

@@ -2,8 +2,9 @@ package app.termora
interface TerminalTabbedManager {
fun addTerminalTab(tab: TerminalTab)
fun addTerminalTab(index: Int, tab: TerminalTab)
fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
}

View File

@@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider {
}
minimumSize = Dimension(640, 400)
terminalTabbed.addTab(welcomePanel)
terminalTabbed.addTerminalTab(welcomePanel)
// macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) {