clipboard implemented

This commit is contained in:
rdavidek 2026-01-17 14:23:36 +01:00
parent cfac7e28ab
commit b28b090fa9
6 changed files with 367 additions and 22 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

View File

@ -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<File> 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<File> getFilesFromClipboard() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
if (clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {
try {
return (List<File>) 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<File> files;
private final ClipboardAction action;
public FileTransferable(List<File> 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);
}
}
}

View File

@ -31,6 +31,11 @@ public class FileOperations {
File source = item.getFile(); File source = item.getFile();
File target = new File(targetDirectory, source.getName()); 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()) { if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback); copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
} else { } else {

View File

@ -1,11 +1,15 @@
package cz.kamma.kfmanager.ui; package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.service.ClipboardService;
import cz.kamma.kfmanager.service.FileOperations;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
@ -836,15 +840,20 @@ public class FilePanelTab extends JPanel {
fileTable.revalidate(); fileTable.revalidate();
fileTable.repaint(); fileTable.repaint();
if (selectFirst && fileTable.getRowCount() > 0) { if (selectFirst && tableModel.items.size() > 0) {
fileTable.setRowSelectionInterval(0, 0); int startIndex = 0;
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); 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(); fileTable.requestFocusInWindow();
}); });
} else { } else {
if (autoSelectFirst && fileTable.getRowCount() > 0) { if (autoSelectFirst && fileTable.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0); int startIndex = 0;
fileTable.setRowSelectionInterval(startIndex, startIndex);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {} try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {}
}); });
@ -1256,6 +1265,28 @@ public class FilePanelTab extends JPanel {
}); });
menu.add(copyPath); 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... // Associate with...
JMenuItem associateItem = new JMenuItem("Associate with..."); JMenuItem associateItem = new JMenuItem("Associate with...");
associateItem.addActionListener(ae -> { associateItem.addActionListener(ae -> {
@ -1275,6 +1306,7 @@ public class FilePanelTab extends JPanel {
if (res == JOptionPane.YES_OPTION) { if (res == JOptionPane.YES_OPTION) {
java.util.List<FileItem> toDelete = new java.util.ArrayList<>(); java.util.List<FileItem> toDelete = new java.util.ArrayList<>();
toDelete.add(item); toDelete.add(item);
final int rememberedIndex = getFocusedItemIndex();
Window parentWindow = SwingUtilities.getWindowAncestor(FilePanelTab.this); Window parentWindow = SwingUtilities.getWindowAncestor(FilePanelTab.this);
ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, "Deleting"); ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, "Deleting");
@ -1292,7 +1324,11 @@ public class FilePanelTab extends JPanel {
}); });
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
progressDialog.dispose(); 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) { } catch (Exception ex) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
@ -1473,6 +1509,118 @@ public class FilePanelTab extends JPanel {
return null; 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<FileItem> selected = getSelectedItems();
if (selected.isEmpty()) return;
List<File> 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<File> files = ClipboardService.getFilesFromClipboard();
if (files == null || files.isEmpty()) return;
ClipboardService.ClipboardAction action = ClipboardService.getClipboardAction();
List<FileItem> 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 * Mark/Select the very last item in the list
*/ */

View File

@ -858,6 +858,38 @@ public class MainWindow extends JFrame {
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW); 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 // Ctrl+E - Command line history
rootPane.registerKeyboardAction(e -> showCommandLineHistory(), rootPane.registerKeyboardAction(e -> showCommandLineHistory(),
KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK), 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 // Also map Shift+Delete on table level
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_DOWN_MASK), "deleteFiles"); .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 @Override
public void keyTyped(KeyEvent e) { public void keyTyped(KeyEvent e) {
char c = e.getKeyChar(); char c = e.getKeyChar();
// Printable characters only (exclude control keys like Enter, Backspace, Esc, Tab) // Transfer to command line only for printable characters and when no modifiers like Ctrl or Alt are pressed.
if (c != KeyEvent.CHAR_UNDEFINED && c != '\b' && c != '\n' && c != '\t' && c != 27) { // 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(); commandLine.requestFocusInWindow();
String current = commandLine.getEditor().getItem().toString(); String current = commandLine.getEditor().getItem().toString();
commandLine.getEditor().setItem(current + c); commandLine.getEditor().setItem(current + c);
@ -1118,9 +1185,9 @@ public class MainWindow extends JFrame {
JOptionPane.INFORMATION_MESSAGE); JOptionPane.INFORMATION_MESSAGE);
return; return;
} }
// remember current selection row so we can restore selection after deletion // remember current selection index so we can restore selection after deletion
JTable table = activePanel != null ? activePanel.getFileTable() : null; final int rememberedIndex = (activePanel != null && activePanel.getCurrentTab() != null) ?
final int rememberedRow = (table != null) ? table.getSelectedRow() : -1; activePanel.getCurrentTab().getFocusedItemIndex() : -1;
StringBuilder message = new StringBuilder("Really delete the following items?\n\n"); StringBuilder message = new StringBuilder("Really delete the following items?\n\n");
for (FileItem item : selectedItems) { for (FileItem item : selectedItems) {
@ -1142,20 +1209,15 @@ public class MainWindow extends JFrame {
FileOperations.delete(selectedItems, callback); FileOperations.delete(selectedItems, callback);
}, "Delete completed", false, activePanel); }, "Delete completed", false, activePanel);
// After deletion and refresh, restore selection: stay on same row if possible, // After deletion and refresh, restore selection: move focus to the nearest higher item.
// otherwise move selection one row up.
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
try { try {
JTable t = activePanel != null ? activePanel.getFileTable() : null; if (activePanel != null && activePanel.getCurrentTab() != null) {
if (t == null) return; // Use another invokeLater to ensure BRIEF mode layout is updated
int rowCount = t.getRowCount(); SwingUtilities.invokeLater(() -> {
if (rowCount == 0) return; activePanel.getCurrentTab().selectItemByIndex(rememberedIndex - 1);
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();
} catch (Exception ignore) {} } catch (Exception ignore) {}
}); });
} else { } else {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB