From df2e9b07431aded6baa06efe840aaa75357b4316 Mon Sep 17 00:00:00 2001 From: hstyi Date: Thu, 23 Jan 2025 16:12:20 +0800 Subject: [PATCH] feat: support drag and drop sorting --- src/main/kotlin/app/termora/MyTabbedPane.kt | 150 ++++++++++++++++++ src/main/kotlin/app/termora/TerminalTabbed.kt | 45 ++++-- .../app/termora/TerminalTabbedManager.kt | 3 +- src/main/kotlin/app/termora/TermoraFrame.kt | 2 +- 4 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/app/termora/MyTabbedPane.kt b/src/main/kotlin/app/termora/MyTabbedPane.kt index c8e6eb1..d3168b2 100644 --- a/src/main/kotlin/app/termora/MyTabbedPane.kt +++ b/src/main/kotlin/app/termora/MyTabbedPane.kt @@ -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 + } + } + + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index f2a8585..f2bd50e 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -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 } } diff --git a/src/main/kotlin/app/termora/TerminalTabbedManager.kt b/src/main/kotlin/app/termora/TerminalTabbedManager.kt index 0ccbda5..f7b4c2e 100644 --- a/src/main/kotlin/app/termora/TerminalTabbedManager.kt +++ b/src/main/kotlin/app/termora/TerminalTabbedManager.kt @@ -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 fun setSelectedTerminalTab(tab: TerminalTab) - fun closeTerminalTab(tab: TerminalTab) + fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true) } \ 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 ce5e5be..6528f20 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider { } minimumSize = Dimension(640, 400) - terminalTabbed.addTab(welcomePanel) + terminalTabbed.addTerminalTab(welcomePanel) // macOS 要避开左边的控制栏 if (SystemInfo.isMacOS) {