diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 3f13ade..0a5aadb 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -22,10 +22,7 @@ import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.SystemUtils import org.json.JSONObject import org.slf4j.LoggerFactory -import java.awt.MenuItem -import java.awt.PopupMenu -import java.awt.SystemTray -import java.awt.TrayIcon +import java.awt.* import java.awt.desktop.AppReopenedEvent import java.awt.desktop.AppReopenedListener import java.awt.desktop.SystemEventListener @@ -202,6 +199,7 @@ class ApplicationRunner { UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) + UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true) UIManager.put("Component.arc", 5) UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc")) @@ -237,12 +235,24 @@ class ApplicationRunner { // Linux 更多的是尖锐风格 if (SystemInfo.isMacOS || SystemInfo.isWindows) { + val selectionInsets = Insets(0, 2, 0, 2) UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("Tree.selectionInsets", selectionInsets) + UIManager.put("List.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("List.selectionInsets", selectionInsets) + UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("ComboBox.selectionInsets", selectionInsets) + UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("Table.selectionInsets", selectionInsets) + UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("MenuBar.selectionInsets", selectionInsets) + UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc")) + UIManager.put("MenuItem.selectionInsets", selectionInsets) } } diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 1c7755c..2d82ebf 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -34,6 +34,7 @@ object Icons { val empty by lazy { DynamicIcon("icons/empty.svg") } val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") } val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") } + val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") } val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") } val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") } val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") } diff --git a/src/main/kotlin/app/termora/terminal/panel/vw/TransferVisualWindow.kt b/src/main/kotlin/app/termora/terminal/panel/vw/TransferVisualWindow.kt index 0698333..0a0dcda 100644 --- a/src/main/kotlin/app/termora/terminal/panel/vw/TransferVisualWindow.kt +++ b/src/main/kotlin/app/termora/terminal/panel/vw/TransferVisualWindow.kt @@ -99,9 +99,11 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi if (key == DataKey.CurrentDir) { val dir = DataKey.CurrentDir.clazz.cast(data) val navigator = getTransportNavigator() ?: return - val path = navigator.getFileSystem().getPath(dir) - if (path == navigator.workdir) return - navigator.navigateTo(path) + val loader = navigator.loader + if (loader.isOpened().not()) return + val fileSystem = loader.getSyncTransportSupport().getFileSystem() + val path = fileSystem.getPath(dir) + navigator.navigateTo(path.absolutePathString()) } } }) @@ -146,14 +148,25 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi try { val session = getSession() val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session) - val support = TransportSupport(fileSystem, fileSystem.defaultDir.absolutePathString()) + val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir) withContext(Dispatchers.Swing) { val internalTransferManager = MyInternalTransferManager() val transportPanel = TransportPanel( internalTransferManager, tab.host, - TransportSupportLoader { support }) - internalTransferManager.setTransferPanel(transportPanel) + object : TransportSupportLoader { + override suspend fun getTransportSupport(): TransportSupport { + return support + } + override fun getSyncTransportSupport(): TransportSupport { + return support + } + + override fun isLoaded(): Boolean { + return true + } + }) + internalTransferManager.setTransferPanel(transportPanel) Disposer.register(transportPanel, object : Disposable { override fun dispose() { panel.remove(transportPanel) diff --git a/src/main/kotlin/app/termora/transfer/DefaultInternalTransferManager.kt b/src/main/kotlin/app/termora/transfer/DefaultInternalTransferManager.kt index 652b5f6..5f970d2 100644 --- a/src/main/kotlin/app/termora/transfer/DefaultInternalTransferManager.kt +++ b/src/main/kotlin/app/termora/transfer/DefaultInternalTransferManager.kt @@ -65,7 +65,9 @@ class DefaultInternalTransferManager( override fun canTransfer(paths: List): Boolean { - return paths.isNotEmpty() && target.getWorkdir() != null + val c = target.getWorkdir() ?: return false + if (c.fileSystem.isOpen.not()) return false + return paths.isNotEmpty() } override fun addTransfer( @@ -270,7 +272,8 @@ class DefaultInternalTransferManager( val isDirectory = pair.second.isDirectory val path = pair.first if (isDirectory.not() || mode == TransferMode.Rmrf) { - val transfer = createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action) + val transfer = + createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action) return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE } diff --git a/src/main/kotlin/app/termora/transfer/DefaultTransportSupport.kt b/src/main/kotlin/app/termora/transfer/DefaultTransportSupport.kt new file mode 100644 index 0000000..e2fadc6 --- /dev/null +++ b/src/main/kotlin/app/termora/transfer/DefaultTransportSupport.kt @@ -0,0 +1,14 @@ +package app.termora.transfer + +import java.nio.file.FileSystem +import java.nio.file.Path + +class DefaultTransportSupport(private val fileSystem: FileSystem, private val defaultPath: Path) : TransportSupport { + override fun getFileSystem(): FileSystem { + return fileSystem + } + + override fun getDefaultPath(): Path { + return defaultPath + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transfer/ReconnectableTransportSupportLoader.kt b/src/main/kotlin/app/termora/transfer/ReconnectableTransportSupportLoader.kt new file mode 100644 index 0000000..c7f8e03 --- /dev/null +++ b/src/main/kotlin/app/termora/transfer/ReconnectableTransportSupportLoader.kt @@ -0,0 +1,97 @@ +package app.termora.transfer + +import app.termora.* +import app.termora.protocol.PathHandler +import app.termora.protocol.PathHandlerRequest +import app.termora.protocol.TransferProtocolProvider +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.slf4j.LoggerFactory +import java.awt.Window +import java.nio.file.FileSystem +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference + +internal class ReconnectableTransportSupportLoader(private val owner: Window, private val host: Host) : + TransportSupportLoader { + companion object { + private val log = LoggerFactory.getLogger(ReconnectableTransportSupportLoader::class.java) + } + + private val mutex = Mutex() + private val reference = AtomicReference() + + private var support: MyTransportSupport? + set(value) = reference.set(value) + get() = reference.get() + + override suspend fun getTransportSupport(): TransportSupport { + mutex.withLock { + var c = support + if (c != null) { + if (c.getFileSystem().isOpen) { + return c + } + if (log.isWarnEnabled) { + log.warn("Host {} has been disconnected and will reconnect soon", host.name) + } + support = null + Disposer.dispose(c) + } + c = connect().also { support = it } + return c + } + } + + override fun getSyncTransportSupport(): TransportSupport { + assertEventDispatchThread() + val c = support + if (c == null) throw IllegalStateException("No transport support") + return c + } + + override fun isLoaded(): Boolean { + assertEventDispatchThread() + return support != null + } + + override fun isOpened(): Boolean { + if (isLoaded().not()) return false + val c = support ?: return false + return c.getFileSystem().isOpen + } + + override fun dispose() { + val c = support + if (c != null) { + Disposer.dispose(c) + } + } + + private fun connect(): MyTransportSupport { + val provider = TransferProtocolProvider.valueOf(host.protocol) + if (provider == null) { + throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol)) + } + val handler = provider.createPathHandler(PathHandlerRequest(host, owner)) + return MyTransportSupport(handler) + } + + + private inner class MyTransportSupport(private val handler: PathHandler) : TransportSupport, Disposable { + + init { + Disposer.register(this, handler) + } + + override fun getFileSystem(): FileSystem { + return handler.fileSystem + } + + override fun getDefaultPath(): Path { + return handler.path + } + + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transfer/TransferTableModel.kt b/src/main/kotlin/app/termora/transfer/TransferTableModel.kt index d6277dc..351a5d1 100644 --- a/src/main/kotlin/app/termora/transfer/TransferTableModel.kt +++ b/src/main/kotlin/app/termora/transfer/TransferTableModel.kt @@ -398,7 +398,9 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) : if (continueTransfer(node, false)) { doTransfer(node) } else { - changeState(node, State.Failed) + withContext(Dispatchers.Swing) { + changeState(node, State.Failed) + } } } lock.withLock { condition.signalAll() } diff --git a/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt b/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt index 394614b..d6a64be 100644 --- a/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt +++ b/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt @@ -2,7 +2,6 @@ package app.termora.transfer import app.termora.DynamicColor import app.termora.Icons -import app.termora.OptionPane import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.FlatSVGIcon @@ -14,8 +13,6 @@ import com.formdev.flatlaf.util.SystemInfo import org.apache.commons.io.FilenameUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils -import org.apache.commons.lang3.exception.ExceptionUtils -import org.slf4j.LoggerFactory import java.awt.CardLayout import java.awt.Dimension import java.awt.Insets @@ -24,7 +21,6 @@ import java.awt.event.* import java.beans.PropertyChangeEvent import java.beans.PropertyChangeListener import java.nio.file.Path -import java.util.function.Supplier import javax.swing.* import javax.swing.event.PopupMenuEvent import javax.swing.event.PopupMenuListener @@ -33,13 +29,9 @@ import kotlin.io.path.name import kotlin.io.path.pathString import kotlin.math.round -class TransportNavigationPanel( - private val support: Supplier, - private val navigator: TransportNavigator -) : JPanel() { +internal class TransportNavigationPanel(private val navigator: TransportNavigator) : JPanel() { companion object { - private val log = LoggerFactory.getLogger(TransportNavigationPanel::class.java) private const val TEXT_FIELD = "TextField" private const val SEGMENTS = "Segments" @@ -50,7 +42,6 @@ class TransportNavigationPanel( } - private val owner get() = SwingUtilities.getWindowAncestor(this) private val layeredPane = LayeredPane() private val textField = FlatTextField() private val downBtn = JButton(Icons.chevronDown) @@ -115,8 +106,7 @@ class TransportNavigationPanel( val itemListener = object : ItemListener { override fun itemStateChanged(e: ItemEvent) { val path = comboBox.selectedItem as Path? ?: return - if (navigator.loading) return - navigator.navigateTo(path) + navigator.navigateTo(path.absolutePathString()) } } @@ -179,17 +169,7 @@ class TransportNavigationPanel( override fun actionPerformed(e: ActionEvent) { if (navigator.loading) return if (textField.text.isBlank()) return - - try { - val path = support.get().fileSystem.getPath(textField.text) - navigator.navigateTo(path) - } catch (e: Exception) { - if (log.isErrorEnabled) log.error(e.message, e) - OptionPane.showMessageDialog( - owner, ExceptionUtils.getRootCauseMessage(e), - messageType = JOptionPane.ERROR_MESSAGE - ) - } + navigator.navigateTo(textField.text) } }) @@ -261,7 +241,7 @@ class TransportNavigationPanel( if (path == navigator.workdir) { setTextFieldText(path) } else { - navigator.navigateTo(path) + navigator.navigateTo(path.absolutePathString()) } } }) @@ -353,7 +333,7 @@ class TransportNavigationPanel( text = item.pathString } } - popupMenu.add(text).addActionListener { navigator.navigateTo(item) } + popupMenu.add(text).addActionListener { navigator.navigateTo(item.absolutePathString()) } } popupMenu.show( button, diff --git a/src/main/kotlin/app/termora/transfer/TransportNavigator.kt b/src/main/kotlin/app/termora/transfer/TransportNavigator.kt index 29135c7..639622a 100644 --- a/src/main/kotlin/app/termora/transfer/TransportNavigator.kt +++ b/src/main/kotlin/app/termora/transfer/TransportNavigator.kt @@ -8,7 +8,7 @@ interface TransportNavigator { val loading: Boolean val workdir: Path? - fun navigateTo(destination: Path): Boolean + fun navigateTo(destination: String): Boolean fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener) fun removePropertyChangeListener(propertyName: String, listener: PropertyChangeListener) diff --git a/src/main/kotlin/app/termora/transfer/TransportPanel.kt b/src/main/kotlin/app/termora/transfer/TransportPanel.kt index 7924890..48ce5cd 100644 --- a/src/main/kotlin/app/termora/transfer/TransportPanel.kt +++ b/src/main/kotlin/app/termora/transfer/TransportPanel.kt @@ -59,9 +59,9 @@ import kotlin.io.path.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -class TransportPanel( +internal class TransportPanel( private val transferManager: InternalTransferManager, - var host: Host, + val host: Host, val loader: TransportSupportLoader, ) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator { companion object { @@ -118,9 +118,6 @@ class TransportPanel( private val disposed = AtomicBoolean(false) private val futures = Collections.synchronizedSet(mutableSetOf>()) - private val _fileSystem by lazy { getSupport().fileSystem } - private val defaultPath by lazy { getSupport().path } - /** * 工作目录 @@ -154,7 +151,7 @@ class TransportPanel( toolbar.add(prevBtn) toolbar.add(homeBtn) toolbar.add(nextBtn) - toolbar.add(TransportNavigationPanel(loader, this)) + toolbar.add(TransportNavigationPanel(this)) toolbar.add(bookmarkBtn) toolbar.add(parentBtn) toolbar.add(eyeBtn) @@ -235,7 +232,7 @@ class TransportPanel( Disposer.register(this, editTransferListener) - refreshBtn.addActionListener { reload() } + refreshBtn.addActionListener { reload(requestFocus = true) } prevBtn.addActionListener { navigator.back() } nextBtn.addActionListener { navigator.forward() } @@ -243,7 +240,7 @@ class TransportPanel( parentBtn.addActionListener(createSmartAction(object : AbstractAction() { override fun actionPerformed(e: ActionEvent) { if (hasParent.not()) return - navigator.navigateTo(model.getPath(0)) + reload(newPath = model.getPath(0).absolutePathString(), requestFocus = true) } })) @@ -258,14 +255,16 @@ class TransportPanel( } bookmarkBtn.isBookmark = bookmarkBtn.isBookmark.not() } else { - navigateTo(_fileSystem.getPath(e.actionCommand)) + navigateTo(e.actionCommand) } } })) homeBtn.addActionListener(createSmartAction(object : AbstractAction() { override fun actionPerformed(e: ActionEvent) { - navigator.navigateTo(_fileSystem.getPath(defaultPath)) + if (loader.isLoaded()) { + navigator.navigateTo(loader.getSyncTransportSupport().getDefaultPath().absolutePathString()) + } } })) @@ -273,7 +272,7 @@ class TransportPanel( override fun actionPerformed(e: ActionEvent) { showHiddenFiles = showHiddenFiles.not() eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose - reload() + reload(requestFocus = true) } })) @@ -289,8 +288,11 @@ class TransportPanel( transferManager.addTransferListener(object : TransferListener { override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) { if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return - if (transfer.target().fileSystem != _fileSystem) return - if (transfer.target() == workdir || transfer.target().parent == workdir) { + val target = transfer.target() + if (loader.isLoaded()) { + if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return + } + if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { reload(requestFocus = false) } } @@ -362,7 +364,7 @@ class TransportPanel( undoManager.addEdit(object : AbstractUndoableEdit() { override fun undo() { super.undo() - if (navigator.navigateTo(oldValue)) { + if (navigator.reload(newPath = oldValue.absolutePathString(), requestFocus = true)) { undoOrRedo = true undoOrRedoPath = oldValue } @@ -370,7 +372,7 @@ class TransportPanel( override fun redo() { super.redo() - if (navigator.navigateTo(newValue)) { + if (navigator.reload(newPath = newValue.absolutePathString(), requestFocus = true)) { undoOrRedo = true undoOrRedoPath = newValue } @@ -431,10 +433,10 @@ class TransportPanel( if (attributes.isDirectory) { enterSelectionFolder() } else { - transferManager.addTransfer( - listOf(model.getPath(row) to attributes), - InternalTransferManager.TransferMode.Transfer - ) + val paths = listOf(model.getPath(row) to attributes) + if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) { + transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer) + } } } else if (SwingUtilities.isRightMouseButton(e)) { val r = table.rowAtPoint(e.point) @@ -524,7 +526,6 @@ class TransportPanel( } private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? { - if (loader.isLoaded.not()) 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) @@ -540,7 +541,8 @@ class TransportPanel( if (transferTransferable.component == panel) return null paths.addAll(transferTransferable.files) } else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { - if (_fileSystem.isLocallyFileSystem()) return null + if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem()) + return null if (load) { val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> if (files.isEmpty()) return null @@ -577,22 +579,12 @@ class TransportPanel( } - fun getTableModel(): TransportTableModel { - return model + private suspend fun getFileSystem(): FileSystem { + return loader.getTransportSupport().getFileSystem() } - fun getFileSystem(): FileSystem { - return _fileSystem - } - - /** - * 不能在 EDT 线程调用 - */ - private fun getSupport(): TransportSupport { - if (SwingUtilities.isEventDispatchThread()) { - throw WrongThreadException("AWT EventQueue") - } - return loader.get() + private suspend fun getTransportSupport(): TransportSupport { + return loader.getTransportSupport() } private fun enterSelectionFolder() { @@ -609,7 +601,7 @@ class TransportPanel( if (workdir != null) registerSelectRow(workdir.name) } - navigator.navigateTo(path) + navigator.navigateTo(path.absolutePathString()) } private fun registerSelectRow(name: String) { @@ -626,7 +618,11 @@ class TransportPanel( } } - private fun reload(oldPath: Path? = workdir, newPath: Path? = workdir, requestFocus: Boolean = false): Boolean { + fun reload( + oldPath: String? = workdir?.absolutePathString(), + newPath: String? = workdir?.absolutePathString(), + requestFocus: Boolean = false + ): Boolean { assertEventDispatchThread() if (loading) return false @@ -662,20 +658,26 @@ class TransportPanel( return true } - private suspend fun doReload(oldPath: Path? = null, newPath: Path? = null, requestFocus: Boolean = false): Path { + private suspend fun doReload( + oldPath: String? = null, + newPath: String? = null, + requestFocus: Boolean = false + ): Path { + val support = getTransportSupport() + val fileSystem = support.getFileSystem() val workdir = newPath ?: oldPath if (workdir == null) { - val path = _fileSystem.getPath(defaultPath) - return doReload(null, path) + val path = support.getDefaultPath() + return doReload(null, path.absolutePathString()) } - val path = workdir + val path = fileSystem.getPath(workdir) val first = AtomicBoolean(false) var parent = path.parent - if (parent == null && _fileSystem.isWindowsFileSystem() && workdir.pathString != _fileSystem.separator) { - parent = _fileSystem.getPath(_fileSystem.separator) + if (parent == null && fileSystem.isWindowsFileSystem() && path.pathString != fileSystem.separator) { + parent = fileSystem.getPath(fileSystem.separator) } val files = mutableListOf>() if ((parent != null).also { hasParent = it }) { @@ -696,8 +698,8 @@ class TransportPanel( files.clear() } - if (_fileSystem.isWindowsFileSystem() && workdir.pathString == _fileSystem.separator) { - for (path in _fileSystem.rootDirectories) { + if (fileSystem.isWindowsFileSystem() && path.pathString == fileSystem.separator) { + for (path in fileSystem.rootDirectories) { val attributes = getAttributes(path) files.add(path to attributes) } @@ -716,7 +718,7 @@ class TransportPanel( if (requestFocus) coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() } - return workdir + return path } private fun listFiles(path: Path): Stream> { @@ -787,21 +789,24 @@ class TransportPanel( } } - private fun showContextmenu(rows: Array, e: MouseEvent) { val files = rows.map { model.getPath(it) to model.getAttributes(it) } - val popupMenu = TransportPopupMenu(owner, model, transferManager, _fileSystem, files) + val popupMenu = TransportPopupMenu(owner, model, transferManager, loader, files) popupMenu.addActionListener(PopupMenuActionListener(files)) popupMenu.show(table, e.x, e.y) } - override fun navigateTo(destination: Path): Boolean { + + override fun navigateTo(destination: String): Boolean { assertEventDispatchThread() if (loading) return false - if (workdir == destination) return false - return reload(workdir, destination) + if (loader.isOpened()) { + if (workdir?.absolutePathString() == destination) return false + } + + return reload(newPath = destination) } override fun getHistory(): List { @@ -825,7 +830,7 @@ class TransportPanel( } private fun setNewWorkdir(destination: Path) { - val oldValue = workdir + val oldValue = if (destination.fileSystem == workdir?.fileSystem) workdir else null workdir = destination firePropertyChange("workdir", oldValue, destination) } @@ -912,8 +917,12 @@ class TransportPanel( val millis = Files.getLastModifiedTime(localPath).toMillis() if (oldMillis == millis) continue + // 正在编辑时可能会出现断线的情况 ,安全获取 + val fs = getFileSystem() + if (fs.isOpen.not()) continue + // 发送到服务器 - transferManager.addHighTransfer(localPath, target) + transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString())) oldMillis = millis } } @@ -1009,8 +1018,9 @@ class TransportPanel( } else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) { val name = e.source.toString() val workdir = workdir ?: return - val path = workdir.resolve(name) processPath(e.source.toString()) { + // 因为此时可能已经断线,任何 Path 都不可完全相信 + val path = getFileSystem().getPath(workdir.resolve(name).absolutePathString()) if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder) path.createDirectories() else @@ -1022,6 +1032,9 @@ class TransportPanel( processPath(e.source.toString()) { source.moveTo(target) } } else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) { transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf) + } else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) { + // reload now + reload() } else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) { val c = e.source as TransportPopupMenu.ChangePermission val path = files.first().first @@ -1104,7 +1117,7 @@ class TransportPanel( private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() { override fun getTableCellRendererComponent( - table: JTable?, + table: JTable, value: Any?, isSelected: Boolean, hasFocus: Boolean, @@ -1133,12 +1146,13 @@ class TransportPanel( text = StringUtils.EMPTY } + foreground = null val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column) icon = null if (column == TransportTableModel.COLUMN_NAME) { - if (_fileSystem.isWindowsFileSystem()) { - val path = model.getPath(sorter.convertRowIndexToModel(row)) + val path = model.getPath(sorter.convertRowIndexToModel(row)) + if (path.fileSystem.isWindowsFileSystem()) { icon = if (attributes.isParent) { NativeIcons.folderIcon } else { @@ -1165,6 +1179,12 @@ class TransportPanel( } } + if (loader.isOpened().not()) { + if (isSelected.not()) { + foreground = UIManager.getColor("textInactiveText") + } + } + return c } diff --git a/src/main/kotlin/app/termora/transfer/TransportPopupMenu.kt b/src/main/kotlin/app/termora/transfer/TransportPopupMenu.kt index a600cbe..bd30522 100644 --- a/src/main/kotlin/app/termora/transfer/TransportPopupMenu.kt +++ b/src/main/kotlin/app/termora/transfer/TransportPopupMenu.kt @@ -6,6 +6,7 @@ import app.termora.Icons import app.termora.OptionPane import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem import com.formdev.flatlaf.extras.components.FlatPopupMenu +import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils import org.apache.sshd.sftp.client.fs.SftpFileSystem import java.awt.Window @@ -13,7 +14,6 @@ import java.awt.datatransfer.StringSelection import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.awt.event.KeyEvent -import java.nio.file.FileSystem import java.nio.file.Path import java.nio.file.attribute.PosixFilePermission import java.util.* @@ -26,11 +26,11 @@ import kotlin.io.path.absolutePathString import kotlin.io.path.name -class TransportPopupMenu( +internal class TransportPopupMenu( private val owner: Window, private val model: TransportTableModel, private val transferManager: InternalTransferManager, - private val fileSystem: FileSystem, + private val loader: TransportSupportLoader, private val files: List> ) : FlatPopupMenu() { private val paths = files.map { it.first } @@ -71,25 +71,45 @@ class TransportPopupMenu( private fun initView() { inheritsPopupMenu = false + if (loader.isOpened().not()) { + val reconnect = add(I18n.getString("termora.tabbed.contextmenu.reconnect")) + reconnect.addActionListener { e -> fireActionPerformed(e, ActionCommand.Reconnect) } + return + } + + val fileSystem = if (loader.isLoaded()) loader.getSyncTransportSupport().getFileSystem() else null + add(transferMenu) add(editMenu) addSeparator() add(copyPathMenu) - if (fileSystem.isLocallyFileSystem()) add(openInFinderMenu) + if (fileSystem?.isLocallyFileSystem() == true) { + add(openInFinderMenu) + } addSeparator() add(renameMenu) add(deleteMenu) - if (fileSystem is SftpFileSystem) add(rmrfMenu) + if (fileSystem is SftpFileSystem) { + add(rmrfMenu) + } add(changePermissionsMenu) addSeparator() add(refreshMenu) addSeparator() add(newMenu) + // 开发环境提供断线 + if (Application.getAppPath().isBlank() && loader.isOpened()) { + addSeparator() + add("Disconnect").addActionListener { + IOUtils.closeQuietly(loader.getSyncTransportSupport().getFileSystem()) + } + } + transferMenu.isEnabled = hasParent.not() && files.isNotEmpty() && transferManager.canTransfer(paths) copyPathMenu.isEnabled = files.isNotEmpty() - openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem() - editMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem().not() + openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() == true + editMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() != true && files.all { it.second.isFile && it.second.isSymbolicLink.not() } renameMenu.isEnabled = hasParent.not() && files.size == 1 deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty() @@ -211,6 +231,7 @@ class TransportPopupMenu( Refresh, ChangePermissions, Rmrf, + Reconnect, } data class ChangePermission(val permissions: Set, val includeSubFolder: Boolean) diff --git a/src/main/kotlin/app/termora/transfer/TransportSelectionPanel.kt b/src/main/kotlin/app/termora/transfer/TransportSelectionPanel.kt index 639e9a7..3fb802d 100644 --- a/src/main/kotlin/app/termora/transfer/TransportSelectionPanel.kt +++ b/src/main/kotlin/app/termora/transfer/TransportSelectionPanel.kt @@ -2,7 +2,6 @@ package app.termora.transfer import app.termora.* import app.termora.database.DatabaseManager -import app.termora.protocol.PathHandlerRequest import app.termora.protocol.TransferProtocolProvider import app.termora.tree.* import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon @@ -24,9 +23,8 @@ import java.util.concurrent.Executors import javax.swing.* import javax.swing.event.TreeExpansionEvent import javax.swing.event.TreeExpansionListener -import kotlin.io.path.absolutePathString -class TransportSelectionPanel( +internal class TransportSelectionPanel( private val tabbed: TransportTabbed, private val transferManager: InternalTransferManager, ) : JPanel(BorderLayout()), Disposable { @@ -99,19 +97,16 @@ class TransportSelectionPanel( private suspend fun doConnect(host: Host) { - val provider = TransferProtocolProvider.valueOf(host.protocol) - if (provider == null) { - throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol)) - } + val loader = ReconnectableTransportSupportLoader(owner, host) - val handler = provider.createPathHandler(PathHandlerRequest(host, owner)) - val support = TransportSupport(handler.fileSystem, handler.path.absolutePathString()) + // try load + loader.getTransportSupport() withContext(Dispatchers.Swing) { - val panel = TransportPanel(transferManager, host, TransportSupportLoader { support }) + val panel = TransportPanel(transferManager, host, loader) Disposer.register(panel, object : Disposable { override fun dispose() { - Disposer.dispose(handler) + Disposer.dispose(loader) } }) swingCoroutineScope.launch { diff --git a/src/main/kotlin/app/termora/transfer/TransportSupport.kt b/src/main/kotlin/app/termora/transfer/TransportSupport.kt index 3bdf3fb..b760776 100644 --- a/src/main/kotlin/app/termora/transfer/TransportSupport.kt +++ b/src/main/kotlin/app/termora/transfer/TransportSupport.kt @@ -1,9 +1,10 @@ package app.termora.transfer import java.nio.file.FileSystem +import java.nio.file.Path -class TransportSupport( - val fileSystem: FileSystem, - val path: String -) \ No newline at end of file +internal interface TransportSupport { + fun getFileSystem(): FileSystem + fun getDefaultPath(): Path +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transfer/TransportSupportLoader.kt b/src/main/kotlin/app/termora/transfer/TransportSupportLoader.kt index 1116f66..8249dd1 100644 --- a/src/main/kotlin/app/termora/transfer/TransportSupportLoader.kt +++ b/src/main/kotlin/app/termora/transfer/TransportSupportLoader.kt @@ -1,52 +1,26 @@ package app.termora.transfer -import okio.withLock -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.locks.ReentrantLock -import java.util.function.Supplier +import app.termora.Disposable -class TransportSupportLoader(private val support: Supplier) : Supplier { - private val loading = AtomicBoolean(false) - private lateinit var mySupport: TransportSupport - private val lock = ReentrantLock() - private val condition = lock.newCondition() - private val exceptionReference = AtomicReference(null) +internal interface TransportSupportLoader : Disposable { - val isLoaded get() = ::mySupport.isInitialized + /** + * 获取传输支持 + */ + suspend fun getTransportSupport(): TransportSupport + /** + * 只有当 [isLoaded] 返回 true 时才能调用,为了不出现问题,只有 EDT 线程才能调用 + */ + fun getSyncTransportSupport(): TransportSupport - override fun get(): TransportSupport { - if (isLoaded) return mySupport - - if (loading.compareAndSet(false, true)) { - try { - mySupport = support.get() - } catch (e: Exception) { - exceptionReference.set(e) - throw e - } finally { - lock.withLock { - loading.set(false) - condition.signalAll() - } - } - } else { - lock.lock() - try { - condition.await() - } finally { - lock.unlock() - } - } - - val exception = exceptionReference.get() - if (exception != null) { - throw exception - } - - return get() - } - + /** + * 是否已经加载,已经加载不表示可以正常使用,它仅证明已经加载可以同步调用 + */ + fun isLoaded(): Boolean + /** + * 快速检查是否已经成功打开 + */ + fun isOpened(): Boolean = true } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transfer/TransportTabbed.kt b/src/main/kotlin/app/termora/transfer/TransportTabbed.kt index 35170d1..5b91a0f 100644 --- a/src/main/kotlin/app/termora/transfer/TransportTabbed.kt +++ b/src/main/kotlin/app/termora/transfer/TransportTabbed.kt @@ -19,7 +19,7 @@ import javax.swing.JToolBar import javax.swing.SwingUtilities @Suppress("DuplicatedCode") -class TransportTabbed( +internal class TransportTabbed( private val transferManager: TransferManager, ) : FlatTabbedPane(), Disposable { private val addBtn = JButton(Icons.add) @@ -64,14 +64,16 @@ class TransportTabbed( // 右键菜单 addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - if (!SwingUtilities.isRightMouseButton(e)) { - return - } - val index = indexAtLocation(e.x, e.y) if (index < 0) return - - showContextMenu(index, e) + if (SwingUtilities.isRightMouseButton(e)) { + showContextMenu(index, e) + } else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { + val tab = getTransportPanel(index) ?: return + if (tab.loader.isOpened().not()) { + tab.reload() + } + } } }) @@ -105,8 +107,9 @@ class TransportTabbed( private fun tabClose(c: TransportPanel): Boolean { if (transferManager.getTransferCount() < 1) return true - if (c.loader.isLoaded.not()) return false - val fileSystem = c.getFileSystem() + val loader = c.loader + if (loader.isLoaded().not()) return false + val fileSystem = loader.getSyncTransportSupport() val transfers = transferManager.getTransfers() .filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem } if (transfers.isEmpty()) return true @@ -137,8 +140,21 @@ class TransportTabbed( fun addLocalTab() { val host = Host(name = "Local", protocol = LocalProtocolProvider.PROTOCOL) - val support = TransportSupport(FileSystems.getDefault(), getDefaultLocalPath()) - val panel = TransportPanel(internalTransferManager, host, TransportSupportLoader { support }) + val fs = FileSystems.getDefault() + val support = DefaultTransportSupport(fs, fs.getPath(getDefaultLocalPath())) + val panel = TransportPanel(internalTransferManager, host, object : TransportSupportLoader { + override suspend fun getTransportSupport(): TransportSupport { + return support + } + + override fun getSyncTransportSupport(): TransportSupport { + return support + } + + override fun isLoaded(): Boolean { + return true + } + }) addTab(I18n.getString("termora.transport.local"), panel) super.setTabClosable(0, false) } @@ -165,20 +181,30 @@ class TransportTabbed( // 编辑 val edit = popupMenu.add(I18n.getString("termora.keymgr.edit")) edit.addActionListener(object : AnAction() { + private val hostManager get() = HostManager.getInstance() private val accountManager get() = AccountManager.getInstance() override fun actionPerformed(evt: AnActionEvent) { val window = evt.window val dialog = NewHostDialogV2( window, - panel.host, + getHost(panel), accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId }) dialog.setLocationRelativeTo(window) dialog.title = panel.host.name dialog.isVisible = true val host = dialog.host ?: return - HostManager.getInstance().addHost(host, DatabaseChangedExtension.Source.Sync) + hostManager.addHost(host, DatabaseChangedExtension.Source.User) setTitleAt(tabIndex, host.name) - panel.host = host + setHost(panel, host) + } + + + private fun getHost(panel: TransportPanel): Host { + return panel.getClientProperty("EditHost") as Host? ?: panel.host + } + + private fun setHost(panel: TransportPanel, host: Host) { + panel.putClientProperty("EditHost", host) } }) diff --git a/src/main/kotlin/app/termora/transfer/TransportTerminalTab.kt b/src/main/kotlin/app/termora/transfer/TransportTerminalTab.kt index 00cd176..de73cf0 100644 --- a/src/main/kotlin/app/termora/transfer/TransportTerminalTab.kt +++ b/src/main/kotlin/app/termora/transfer/TransportTerminalTab.kt @@ -3,18 +3,18 @@ package app.termora.transfer import app.termora.* import app.termora.database.DatabaseManager import app.termora.terminal.DataKey +import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem import java.beans.PropertyChangeListener -import java.nio.file.FileSystems import javax.swing.Icon import javax.swing.JComponent import javax.swing.JOptionPane import javax.swing.SwingUtilities -class TransportTerminalTab : RememberFocusTerminalTab() { +internal class TransportTerminalTab : RememberFocusTerminalTab() { private val transportViewer = TransportViewer() private val sftp get() = DatabaseManager.getInstance().sftp private val transferManager get() = transportViewer.getTransferManager() - val leftTabbed get() = transportViewer.getLeftTabbed() + private val leftTabbed get() = transportViewer.getLeftTabbed() val rightTabbed get() = transportViewer.getRightTabbed() init { @@ -66,8 +66,8 @@ class TransportTerminalTab : RememberFocusTerminalTab() { private fun hasActiveTab(tabbed: TransportTabbed): Boolean { for (i in 0 until tabbed.tabCount) { val c = tabbed.getComponentAt(i) ?: continue - if (c is TransportPanel && c.loader.isLoaded) { - if (c.getFileSystem() != FileSystems.getDefault()) { + if (c is TransportPanel && c.loader.isOpened()) { + if (c.loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem().not()) { return true } } diff --git a/src/main/kotlin/app/termora/transfer/TransportViewer.kt b/src/main/kotlin/app/termora/transfer/TransportViewer.kt index 953d3f2..a219568 100644 --- a/src/main/kotlin/app/termora/transfer/TransportViewer.kt +++ b/src/main/kotlin/app/termora/transfer/TransportViewer.kt @@ -3,22 +3,19 @@ package app.termora.transfer import app.termora.Disposable import app.termora.Disposer import app.termora.DynamicColor +import app.termora.Icons import app.termora.actions.DataProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import org.slf4j.LoggerFactory +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing import java.awt.BorderLayout import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import java.nio.file.Path import javax.swing.* +import kotlin.time.Duration.Companion.milliseconds -class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable { - companion object { - private val log = LoggerFactory.getLogger(TransportViewer::class.java) - } +internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable { private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val splitPane = JSplitPane() @@ -78,10 +75,33 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable { } }) + + coroutineScope.launch(Dispatchers.Swing) { + while (isActive) { + delay(250.milliseconds) + checkDisconnected(leftTabbed) + checkDisconnected(rightTabbed) + } + } + Disposer.register(this, leftTabbed) Disposer.register(this, rightTabbed) } + private fun checkDisconnected(tabbed: TransportTabbed) { + for (i in 0 until tabbed.tabCount) { + val tab = tabbed.getTransportPanel(i) ?: continue + val icon = tabbed.getIconAt(i) + if (tab.loader.isOpened()) { + if (icon == null) continue + tabbed.setIconAt(i, null) + } else { + if (icon == Icons.breakpoint) continue + tabbed.setIconAt(i, Icons.breakpoint) + } + } + } + private fun createInternalTransferManager( source: TransportTabbed, target: TransportTabbed @@ -118,4 +138,8 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable { return rightTabbed } + override fun dispose() { + coroutineScope.cancel() + } + } \ No newline at end of file diff --git a/src/main/resources/icons/breakpoint.svg b/src/main/resources/icons/breakpoint.svg new file mode 100644 index 0000000..120fb2e --- /dev/null +++ b/src/main/resources/icons/breakpoint.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/breakpoint_dark.svg b/src/main/resources/icons/breakpoint_dark.svg new file mode 100644 index 0000000..17ddd64 --- /dev/null +++ b/src/main/resources/icons/breakpoint_dark.svg @@ -0,0 +1,4 @@ + + + +