diff --git a/kfmanager.png b/kfmanager.png deleted file mode 100644 index 3f025c5..0000000 Binary files a/kfmanager.png and /dev/null differ diff --git a/src/main/java/cz/kamma/kfmanager/service/ClipboardService.java b/src/main/java/cz/kamma/kfmanager/service/ClipboardService.java new file mode 100644 index 0000000..e0285a5 --- /dev/null +++ b/src/main/java/cz/kamma/kfmanager/service/ClipboardService.java @@ -0,0 +1,130 @@ +package cz.kamma.kfmanager.service; + +import java.awt.Toolkit; +import java.awt.datatransfer.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ClipboardService { + + public enum ClipboardAction { + COPY, CUT + } + + public static void copyToClipboard(List files, ClipboardAction action) { + if (files == null || files.isEmpty()) return; + + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + FileTransferable transferable = new FileTransferable(files, action); + clipboard.setContents(transferable, null); + } + + @SuppressWarnings("unchecked") + public static List getFilesFromClipboard() { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + if (clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { + try { + return (List) clipboard.getData(DataFlavor.javaFileListFlavor); + } catch (UnsupportedFlavorException | IOException e) { + e.printStackTrace(); + } + } + return new ArrayList<>(); + } + + public static ClipboardAction getClipboardAction() { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + + // Check for GNOME/KDE flavored cut/copy + try { + for (DataFlavor flavor : clipboard.getAvailableDataFlavors()) { + if ("x-special/gnome-copied-files".equals(flavor.getSubType())) { + String data = (String) clipboard.getData(flavor); + if (data != null && data.startsWith("cut")) { + return ClipboardAction.CUT; + } + return ClipboardAction.COPY; + } + } + } catch (Exception ignore) {} + + // Check for Windows "Preferred DropEffect" + try { + for (DataFlavor flavor : clipboard.getAvailableDataFlavors()) { + if ("application/x-java-remote-object".equals(flavor.getPrimaryType()) && + Integer.class.isAssignableFrom(flavor.getRepresentationClass()) && + "Preferred DropEffect".equals(flavor.getHumanPresentableName())) { + Integer effect = (Integer) clipboard.getData(flavor); + if (effect != null && (effect & 2) != 0) { // 2 = MOVE + return ClipboardAction.CUT; + } + } + } + } catch (Exception ignore) {} + + // Internal fallback (if we set it ourselves) + if (clipboard.isDataFlavorAvailable(FileTransferable.ACTION_FLAVOR)) { + try { + return (ClipboardAction) clipboard.getData(FileTransferable.ACTION_FLAVOR); + } catch (UnsupportedFlavorException | IOException e) { + e.printStackTrace(); + } + } + + return ClipboardAction.COPY; + } + + private static class FileTransferable implements Transferable { + public static final DataFlavor ACTION_FLAVOR = new DataFlavor(ClipboardAction.class, "Clipboard Action"); + private final List files; + private final ClipboardAction action; + + public FileTransferable(List files, ClipboardAction action) { + this.files = files; + this.action = action; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + try { + return new DataFlavor[]{ + DataFlavor.javaFileListFlavor, + ACTION_FLAVOR, + new DataFlavor("x-special/gnome-copied-files;class=java.lang.String") + }; + } catch (ClassNotFoundException e) { + return new DataFlavor[]{ + DataFlavor.javaFileListFlavor, + ACTION_FLAVOR + }; + } + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + for (DataFlavor f : getTransferDataFlavors()) { + if (f.equals(flavor)) return true; + } + return false; + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (DataFlavor.javaFileListFlavor.equals(flavor)) { + return files; + } else if (ACTION_FLAVOR.equals(flavor)) { + return action; + } else if ("x-special".equals(flavor.getPrimaryType()) && "gnome-copied-files".equals(flavor.getSubType())) { + StringBuilder sb = new StringBuilder(); + sb.append(action == ClipboardAction.CUT ? "cut" : "copy"); + for (File f : files) { + sb.append("\n").append(f.toURI().toString()); + } + return sb.toString(); + } + throw new UnsupportedFlavorException(flavor); + } + } +} diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java index 5179b16..e578bbb 100644 --- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java +++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java @@ -30,6 +30,11 @@ public class FileOperations { if (callback != null && callback.isCancelled()) break; File source = item.getFile(); File target = new File(targetDirectory, source.getName()); + + // If target is the same as source (copying to the same directory), rename target + if (source.getAbsolutePath().equals(target.getAbsolutePath())) { + target = new File(targetDirectory, "copy-of-" + source.getName()); + } if (source.isDirectory()) { copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback); diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index f5e5739..2d3db90 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -1,11 +1,15 @@ package cz.kamma.kfmanager.ui; import cz.kamma.kfmanager.model.FileItem; +import cz.kamma.kfmanager.service.ClipboardService; +import cz.kamma.kfmanager.service.FileOperations; import javax.swing.*; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; @@ -836,15 +840,20 @@ public class FilePanelTab extends JPanel { fileTable.revalidate(); fileTable.repaint(); - if (selectFirst && fileTable.getRowCount() > 0) { - fileTable.setRowSelectionInterval(0, 0); - fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + if (selectFirst && tableModel.items.size() > 0) { + int startIndex = 0; + int selRow = startIndex % tableModel.briefRowsPerColumn; + int selCol = startIndex / tableModel.briefRowsPerColumn; + briefCurrentColumn = selCol; + fileTable.setRowSelectionInterval(selRow, selRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true)); } fileTable.requestFocusInWindow(); }); } else { if (autoSelectFirst && fileTable.getRowCount() > 0) { - fileTable.setRowSelectionInterval(0, 0); + int startIndex = 0; + fileTable.setRowSelectionInterval(startIndex, startIndex); SwingUtilities.invokeLater(() -> { try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {} }); @@ -1256,6 +1265,28 @@ public class FilePanelTab extends JPanel { }); menu.add(copyPath); + menu.addSeparator(); + + // Cut + JMenuItem cutItem = new JMenuItem("Cut"); + cutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK)); + cutItem.addActionListener(ae -> copyToClipboard(true)); + menu.add(cutItem); + + // Copy + JMenuItem copyItem = new JMenuItem("Copy"); + copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK)); + copyItem.addActionListener(ae -> copyToClipboard(false)); + menu.add(copyItem); + + // Paste + JMenuItem pasteItem = new JMenuItem("Paste"); + pasteItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK)); + pasteItem.addActionListener(ae -> pasteFromClipboard()); + menu.add(pasteItem); + + menu.addSeparator(); + // Associate with... JMenuItem associateItem = new JMenuItem("Associate with..."); associateItem.addActionListener(ae -> { @@ -1275,6 +1306,7 @@ public class FilePanelTab extends JPanel { if (res == JOptionPane.YES_OPTION) { java.util.List toDelete = new java.util.ArrayList<>(); toDelete.add(item); + final int rememberedIndex = getFocusedItemIndex(); Window parentWindow = SwingUtilities.getWindowAncestor(FilePanelTab.this); ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, "Deleting"); @@ -1292,7 +1324,11 @@ public class FilePanelTab extends JPanel { }); SwingUtilities.invokeLater(() -> { progressDialog.dispose(); - loadDirectory(getCurrentDirectory()); + loadDirectory(getCurrentDirectory(), false); + // Use another invokeLater to ensure BRIEF mode layout is updated + SwingUtilities.invokeLater(() -> { + selectItemByIndex(rememberedIndex - 1); + }); }); } catch (Exception ex) { SwingUtilities.invokeLater(() -> { @@ -1473,6 +1509,118 @@ public class FilePanelTab extends JPanel { return null; } + public int getFocusedItemIndex() { + int row = fileTable.getSelectedRow(); + if (row < 0) return -1; + if (viewMode == ViewMode.BRIEF) { + return briefCurrentColumn * tableModel.briefRowsPerColumn + row; + } else { + return row; + } + } + + public void selectItemByIndex(int index) { + if (index < 0) index = 0; + if (index >= tableModel.items.size()) index = tableModel.items.size() - 1; + if (index < 0) return; + + if (viewMode == ViewMode.BRIEF) { + briefCurrentColumn = index / tableModel.briefRowsPerColumn; + int row = index % tableModel.briefRowsPerColumn; + if (briefCurrentColumn < fileTable.getColumnCount()) { + fileTable.setRowSelectionInterval(row, row); + fileTable.scrollRectToVisible(fileTable.getCellRect(row, briefCurrentColumn, true)); + } + } else { + fileTable.setRowSelectionInterval(index, index); + fileTable.scrollRectToVisible(fileTable.getCellRect(index, 0, true)); + } + fileTable.repaint(); + fileTable.requestFocusInWindow(); + updateStatus(); + } + + public void copyToClipboard(boolean cut) { + List selected = getSelectedItems(); + if (selected.isEmpty()) return; + + List files = new ArrayList<>(); + for (FileItem item : selected) { + files.add(item.getFile()); + } + + ClipboardService.copyToClipboard(files, + cut ? ClipboardService.ClipboardAction.CUT : ClipboardService.ClipboardAction.COPY); + } + + public void pasteFromClipboard() { + List files = ClipboardService.getFilesFromClipboard(); + if (files == null || files.isEmpty()) return; + + ClipboardService.ClipboardAction action = ClipboardService.getClipboardAction(); + + List itemsToPaste = new ArrayList<>(); + for (File f : files) { + itemsToPaste.add(new FileItem(f)); + } + + File targetDir = getCurrentDirectory(); + Window parentWindow = SwingUtilities.getWindowAncestor(this); + String operationName = action == ClipboardService.ClipboardAction.CUT ? "Moving" : "Copying"; + ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, operationName); + + new Thread(() -> { + try { + if (action == ClipboardService.ClipboardAction.CUT) { + FileOperations.move(itemsToPaste, targetDir, new FileOperations.ProgressCallback() { + @Override + public void onProgress(long current, long total, String currentFile) { + progressDialog.updateProgress(current, total, currentFile); + } + @Override + public boolean isCancelled() { + return progressDialog.isCancelled(); + } + }); + } else { + FileOperations.copy(itemsToPaste, targetDir, new FileOperations.ProgressCallback() { + @Override + public void onProgress(long current, long total, String currentFile) { + progressDialog.updateProgress(current, total, currentFile); + } + @Override + public boolean isCancelled() { + return progressDialog.isCancelled(); + } + }); + } + SwingUtilities.invokeLater(() -> { + progressDialog.dispose(); + loadDirectory(targetDir, false); + if (!itemsToPaste.isEmpty()) { + String nameToSelect = itemsToPaste.get(0).getName(); + File firstSource = itemsToPaste.get(0).getFile(); + // Check if we were copying within the same directory - if so, it was renamed to copy-of-... + if (action == ClipboardService.ClipboardAction.COPY && + firstSource.getParentFile() != null && + firstSource.getParentFile().getAbsolutePath().equals(targetDir.getAbsolutePath())) { + nameToSelect = "copy-of-" + nameToSelect; + } + final String finalName = nameToSelect; + // Use invokeLater to ensure layout (especially BRIEF mode) is updated before selection + SwingUtilities.invokeLater(() -> selectItem(finalName)); + } + }); + } catch (Exception ex) { + SwingUtilities.invokeLater(() -> { + progressDialog.dispose(); + JOptionPane.showMessageDialog(this, "Paste failed: " + ex.getMessage()); + }); + } + }).start(); + progressDialog.setVisible(true); + } + /** * Mark/Select the very last item in the list */ diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index 47873c8..84a2325 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -858,6 +858,38 @@ public class MainWindow extends JFrame { KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); + // Ctrl+` - Home directory + rootPane.registerKeyboardAction(e -> { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().loadDirectory(new File(System.getProperty("user.home"))); + } + }, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_QUOTE, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+C - Copy to clipboard + rootPane.registerKeyboardAction(e -> { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().copyToClipboard(false); + } + }, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+X - Cut to clipboard + rootPane.registerKeyboardAction(e -> { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().copyToClipboard(true); + } + }, KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+V - Paste from clipboard + rootPane.registerKeyboardAction(e -> { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().pasteFromClipboard(); + } + }, KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + // Ctrl+E - Command line history rootPane.registerKeyboardAction(e -> showCommandLineHistory(), KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK), @@ -981,6 +1013,40 @@ public class MainWindow extends JFrame { // Also map Shift+Delete on table level table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_DOWN_MASK), "deleteFiles"); + + // Clipboard support (Ctrl+C, Ctrl+X, Ctrl+V) + table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK), "clipboardCopy"); + table.getActionMap().put("clipboardCopy", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().copyToClipboard(false); + } + } + }); + + table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), "clipboardCut"); + table.getActionMap().put("clipboardCut", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().copyToClipboard(true); + } + } + }); + + table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), "clipboardPaste"); + table.getActionMap().put("clipboardPaste", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + if (activePanel != null && activePanel.getCurrentTab() != null) { + activePanel.getCurrentTab().pasteFromClipboard(); + } + } + }); } /** @@ -1011,8 +1077,9 @@ public class MainWindow extends JFrame { @Override public void keyTyped(KeyEvent e) { char c = e.getKeyChar(); - // Printable characters only (exclude control keys like Enter, Backspace, Esc, Tab) - if (c != KeyEvent.CHAR_UNDEFINED && c != '\b' && c != '\n' && c != '\t' && c != 27) { + // Transfer to command line only for printable characters and when no modifiers like Ctrl or Alt are pressed. + // This prevents focus jump on shortcuts like Ctrl+C, Ctrl+V, etc. + if (c != KeyEvent.CHAR_UNDEFINED && c >= 32 && c != 127 && !e.isControlDown() && !e.isAltDown()) { commandLine.requestFocusInWindow(); String current = commandLine.getEditor().getItem().toString(); commandLine.getEditor().setItem(current + c); @@ -1118,9 +1185,9 @@ public class MainWindow extends JFrame { JOptionPane.INFORMATION_MESSAGE); return; } - // remember current selection row so we can restore selection after deletion - JTable table = activePanel != null ? activePanel.getFileTable() : null; - final int rememberedRow = (table != null) ? table.getSelectedRow() : -1; + // remember current selection index so we can restore selection after deletion + final int rememberedIndex = (activePanel != null && activePanel.getCurrentTab() != null) ? + activePanel.getCurrentTab().getFocusedItemIndex() : -1; StringBuilder message = new StringBuilder("Really delete the following items?\n\n"); for (FileItem item : selectedItems) { @@ -1142,20 +1209,15 @@ public class MainWindow extends JFrame { FileOperations.delete(selectedItems, callback); }, "Delete completed", false, activePanel); - // After deletion and refresh, restore selection: stay on same row if possible, - // otherwise move selection one row up. + // After deletion and refresh, restore selection: move focus to the nearest higher item. SwingUtilities.invokeLater(() -> { try { - JTable t = activePanel != null ? activePanel.getFileTable() : null; - if (t == null) return; - int rowCount = t.getRowCount(); - if (rowCount == 0) return; - int targetRow = rememberedRow; - if (targetRow < 0) targetRow = 0; - if (targetRow >= rowCount) targetRow = rowCount - 1; // move up if needed - t.setRowSelectionInterval(targetRow, targetRow); - t.scrollRectToVisible(t.getCellRect(targetRow, 0, true)); - t.requestFocusInWindow(); + if (activePanel != null && activePanel.getCurrentTab() != null) { + // Use another invokeLater to ensure BRIEF mode layout is updated + SwingUtilities.invokeLater(() -> { + activePanel.getCurrentTab().selectItemByIndex(rememberedIndex - 1); + }); + } } catch (Exception ignore) {} }); } else { diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png index 4e72821..97a8e13 100644 Binary files a/src/main/resources/icon.png and b/src/main/resources/icon.png differ