mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support drag and drop sorting
This commit is contained in:
@@ -1,12 +1,162 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
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() {
|
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) {
|
override fun setSelectedIndex(index: Int) {
|
||||||
val oldIndex = selectedIndex
|
val oldIndex = selectedIndex
|
||||||
super.setSelectedIndex(index)
|
super.setSelectedIndex(index)
|
||||||
firePropertyChange("selectedIndex", oldIndex, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,12 +10,14 @@ import app.termora.transport.TransportPanel
|
|||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.AWTEventListener
|
import java.awt.event.AWTEventListener
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -30,7 +32,7 @@ class TerminalTabbed(
|
|||||||
private val toolbar = termoraToolBar.getJToolBar()
|
private val toolbar = termoraToolBar.getJToolBar()
|
||||||
private val actionManager = ActionManager.getInstance()
|
private val actionManager = ActionManager.getInstance()
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -190,16 +192,16 @@ class TerminalTabbed(
|
|||||||
// 修改名称
|
// 修改名称
|
||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
|
||||||
val dialog = InputDialog(
|
val dialog = InputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(index),
|
text = tabbedPane.getTitleAt(tabIndex),
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
val text = dialog.getText()
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(index, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
|
c.putClientProperty(titleProperty, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +278,8 @@ class TerminalTabbed(
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
reconnect.addActionListener {
|
reconnect.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
tabs[tabIndex].reconnect()
|
||||||
tabs[index].reconnect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,18 +290,24 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun addTab(tab: TerminalTab) {
|
private fun addTab(index: Int, tab: TerminalTab) {
|
||||||
tabbedPane.addTab(
|
val c = tab.getJComponent()
|
||||||
tab.getTitle(),
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
|
tabbedPane.insertTab(
|
||||||
|
title,
|
||||||
tab.getIcon(),
|
tab.getIcon(),
|
||||||
tab.getJComponent()
|
c,
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
index
|
||||||
)
|
)
|
||||||
|
c.putClientProperty(titleProperty, title)
|
||||||
|
|
||||||
// 监听 icons 变化
|
// 监听 icons 变化
|
||||||
tab.addPropertyChangeListener(iconListener)
|
tab.addPropertyChangeListener(iconListener)
|
||||||
|
|
||||||
tabs.add(tab)
|
tabs.add(index, tab)
|
||||||
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
|
tabbedPane.selectedIndex = index
|
||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +400,11 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(tab: TerminalTab) {
|
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? {
|
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) {
|
for (i in 0 until tabs.size) {
|
||||||
if (tabs[i] == tab) {
|
if (tabs[i] == tab) {
|
||||||
removeTabAt(i, true)
|
removeTabAt(i, disposable)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
interface TerminalTabbedManager {
|
interface TerminalTabbedManager {
|
||||||
fun addTerminalTab(tab: TerminalTab)
|
fun addTerminalTab(tab: TerminalTab)
|
||||||
|
fun addTerminalTab(index: Int, tab: TerminalTab)
|
||||||
fun getSelectedTerminalTab(): TerminalTab?
|
fun getSelectedTerminalTab(): TerminalTab?
|
||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
fun closeTerminalTab(tab: TerminalTab)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minimumSize = Dimension(640, 400)
|
minimumSize = Dimension(640, 400)
|
||||||
terminalTabbed.addTab(welcomePanel)
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// macOS 要避开左边的控制栏
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
|
|||||||
Reference in New Issue
Block a user