feat: transfer supports copy and paste

This commit is contained in:
hstyi
2025-07-14 11:52:21 +08:00
committed by hstyi
parent a7aec52f2a
commit 8acfdb8bca
6 changed files with 74 additions and 12 deletions

View File

@@ -467,6 +467,15 @@ internal class TransportPanel(
} }
}) })
table.actionMap.put("copy", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
if (files.any { it.second.isParent }) return
toolkit.systemClipboard.setContents(TransferTransferable(panel, files), null)
}
})
table.actionMap.put("Reload", object : AbstractAction() { table.actionMap.put("Reload", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
reload() reload()
@@ -514,7 +523,6 @@ internal class TransportPanel(
data class TransferData( data class TransferData(
// true 就是本地拖拽上传 // true 就是本地拖拽上传
val locally: Boolean, val locally: Boolean,
val row: Int,
val insertRow: Boolean, val insertRow: Boolean,
val workdir: Path, val workdir: Path,
val files: List<Pair<Path, Attributes>> val files: List<Pair<Path, Attributes>>
@@ -540,18 +548,22 @@ internal class TransportPanel(
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? { private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
val workdir = workdir ?: return null val workdir = workdir ?: return null
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
if (hasParent && dropLocation.row == 0) return null
val paths = mutableListOf<Pair<Path, Attributes>>() val paths = mutableListOf<Pair<Path, Attributes>>()
var locally = false var locally = false
if (support.isDataFlavorSupported(TransferTransferable.FLAVOR)) { if (support.isDataFlavorSupported(TransferTransferable.FLAVOR)) {
val transferTransferable = support.transferable.getTransferData(TransferTransferable.FLAVOR) val transferTransferable = support.transferable.getTransferData(TransferTransferable.FLAVOR)
as? TransferTransferable ?: return null as? TransferTransferable ?: return null
if (support.isDrop) {
if (transferTransferable.component == panel) return null if (transferTransferable.component == panel) return null
} else {
// 如果在一个目录,那么是不允许粘贴的
for (pair in transferTransferable.files) {
if (pair.first.parent?.pathString == workdir.pathString) {
return null
}
}
}
paths.addAll(transferTransferable.files) paths.addAll(transferTransferable.files)
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { } else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem()) if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
@@ -569,15 +581,29 @@ internal class TransportPanel(
return null return null
} }
if (support.isDrop) {
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
if (dropLocation.isInsertRow.not() && dropLocation.column != TransportTableModel.COLUMN_NAME) return null
if (dropLocation.isInsertRow.not() && model.getAttributes(row).isDirectory.not()) return null
if (hasParent && dropLocation.row == 0) return null
return TransferData( return TransferData(
locally = locally, locally = locally,
row = row,
insertRow = dropLocation.isInsertRow, insertRow = dropLocation.isInsertRow,
workdir = if (dropLocation.isInsertRow) workdir else model.getPath(row), workdir = if (dropLocation.isInsertRow) workdir else model.getPath(row),
files = paths files = paths
) )
} }
return TransferData(
locally = locally,
insertRow = false,
workdir = workdir,
files = paths
)
}
override fun getSourceActions(c: JComponent?): Int { override fun getSourceActions(c: JComponent?): Int {
return COPY return COPY
} }
@@ -899,7 +925,7 @@ internal class TransportPanel(
} }
} }
private class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) : class TransferTransferable(val component: TransportPanel, val files: List<Pair<Path, Attributes>>) :
Transferable { Transferable {
companion object { companion object {
val FLAVOR = DataFlavor("termora/transfers", "Termora transfers") val FLAVOR = DataFlavor("termora/transfers", "Termora transfers")
@@ -1041,7 +1067,6 @@ internal class TransportPanel(
} }
private inner class PopupMenuActionListener(private val files: List<Pair<Path, Attributes>>) : ActionListener { private inner class PopupMenuActionListener(private val files: List<Pair<Path, Attributes>>) : ActionListener {
@Suppress("CascadeIf")
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
val actionCommand = TransportPopupMenu.ActionCommand.valueOf(e.actionCommand) val actionCommand = TransportPopupMenu.ActionCommand.valueOf(e.actionCommand)
if (actionCommand == TransportPopupMenu.ActionCommand.Transfer) { if (actionCommand == TransportPopupMenu.ActionCommand.Transfer) {
@@ -1089,6 +1114,12 @@ internal class TransportPanel(
Files.setPosixFilePermissions(path, c.permissions) Files.setPosixFilePermissions(path, c.permissions)
} }
} }
} else if (actionCommand == TransportPopupMenu.ActionCommand.Copy) {
val transferable = TransferTransferable(panel, files)
toolkit.systemClipboard.setContents(transferable, null)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Paste) {
val transferable = toolkit.systemClipboard.getContents(null) ?: return
table.transferHandler.importData(TransferHandler.TransferSupport(table, transferable))
} }
} }

View File

@@ -22,6 +22,8 @@ import javax.swing.JMenu
import javax.swing.JMenuItem import javax.swing.JMenuItem
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.event.EventListenerList import javax.swing.event.EventListenerList
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.name import kotlin.io.path.name
@@ -39,6 +41,8 @@ internal class TransportPopupMenu(
private val transferMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.transfer")) private val transferMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.transfer"))
private val editMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.edit")) private val editMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.edit"))
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path")) private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder")) private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename")) private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete")) private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
@@ -82,6 +86,9 @@ internal class TransportPopupMenu(
add(transferMenu) add(transferMenu)
add(editMenu) add(editMenu)
addSeparator() addSeparator()
add(copyMenu)
add(pasteMenu)
addSeparator()
add(copyPathMenu) add(copyPathMenu)
if (fileSystem?.isLocallyFileSystem() == true) { if (fileSystem?.isLocallyFileSystem() == true) {
add(openInFinderMenu) add(openInFinderMenu)
@@ -133,6 +140,7 @@ internal class TransportPopupMenu(
renameMenu.isEnabled = hasParent.not() && files.size == 1 renameMenu.isEnabled = hasParent.not() && files.size == 1
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty() deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1 changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
copyMenu.isEnabled = hasParent.not() && files.isNotEmpty()
for ((item, mnemonic) in mnemonics) { for ((item, mnemonic) in mnemonics) {
item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})" item.text = "${item.text}(${KeyEvent.getKeyText(mnemonic)})"
@@ -166,6 +174,22 @@ internal class TransportPopupMenu(
sb.deleteCharAt(sb.length - 1) sb.deleteCharAt(sb.length - 1)
toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null) toolkit.systemClipboard.setContents(StringSelection(sb.toString()), null)
} }
copyMenu.addActionListener { fireActionPerformed(it, ActionCommand.Copy) }
pasteMenu.addActionListener { fireActionPerformed(it, ActionCommand.Paste) }
addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
pasteMenu.isEnabled = toolkit.systemClipboard
.isDataFlavorAvailable(TransportPanel.TransferTransferable.FLAVOR)
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
}
})
} }
fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) { fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
@@ -241,6 +265,8 @@ internal class TransportPopupMenu(
ChangePermissions, ChangePermissions,
Rmrf, Rmrf,
Reconnect, Reconnect,
Paste,
Copy,
} }
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean) data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)

View File

@@ -3,6 +3,7 @@ termora.confirm=OK
termora.exit=退出 termora.exit=退出
termora.cancel=Cancel termora.cancel=Cancel
termora.copy=Copy termora.copy=Copy
termora.paste=Paste
termora.apply=Apply termora.apply=Apply
termora.save=Save termora.save=Save
termora.remove=Delete termora.remove=Delete

View File

@@ -3,6 +3,7 @@ termora.confirm=Ок
termora.exit=покидать termora.exit=покидать
termora.cancel=Отмена termora.cancel=Отмена
termora.copy=Копировать termora.copy=Копировать
termora.paste=Вставить
termora.apply=Применить termora.apply=Применить
termora.save=Сохранить termora.save=Сохранить
termora.remove=Удалить termora.remove=Удалить

View File

@@ -2,6 +2,7 @@ termora.confirm=确认
termora.exit=退出 termora.exit=退出
termora.cancel=取消 termora.cancel=取消
termora.copy=复制 termora.copy=复制
termora.paste=粘贴
termora.apply=应用 termora.apply=应用
termora.save=保存 termora.save=保存
termora.remove=删除 termora.remove=删除

View File

@@ -1,6 +1,8 @@
termora.confirm=確定 termora.confirm=確定
termora.exit=Exit termora.exit=Exit
termora.cancel=取消 termora.cancel=取消
termora.copy=複製
termora.paste=貼上
termora.apply=应用 termora.apply=应用
termora.save=儲存 termora.save=儲存
termora.remove=刪除 termora.remove=刪除