fixed follow link, selection improved

This commit is contained in:
rdavidek 2026-01-21 21:12:20 +01:00
parent 03472ae2e4
commit a731023ed0
5 changed files with 317 additions and 82 deletions

View File

@ -147,6 +147,11 @@ public class FileOperationQueue {
return FileOperations.OverwriteResponse.YES; return FileOperations.OverwriteResponse.YES;
} }
@Override
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
return FileOperations.SymlinkResponse.IGNORE;
}
@Override @Override
public FileOperations.ErrorResponse onError(File file, Exception e) { public FileOperations.ErrorResponse onError(File file, Exception e) {
// For background queue, maybe skip on error? // For background queue, maybe skip on error?

View File

@ -6,7 +6,9 @@ import java.io.*;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.*; import java.util.zip.*;
@ -24,9 +26,19 @@ public class FileOperations {
} }
List<FileItem> cleanedItems = cleanDuplicateItems(items); List<FileItem> cleanedItems = cleanDuplicateItems(items);
// Sort items so symlinks are last
cleanedItems.sort((a, b) -> {
boolean aSym = Files.isSymbolicLink(a.getFile().toPath());
boolean bSym = Files.isSymbolicLink(b.getFile().toPath());
if (aSym && !bSym) return 1;
if (!aSym && bSym) return -1;
return 0;
});
long totalItems = calculateTotalItems(cleanedItems); long totalItems = calculateTotalItems(cleanedItems);
final long[] currentItem = {0}; final long[] currentItem = {0};
final OverwriteResponse[] globalResponse = {null}; final OverwriteResponse[] globalResponse = {null};
final SymlinkResponse[] globalSymlinkResponse = {null};
for (FileItem item : cleanedItems) { for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break; if (callback != null && callback.isCancelled()) break;
@ -64,15 +76,48 @@ public class FileOperations {
while (true) { while (true) {
try { try {
if (Files.isSymbolicLink(source.toPath())) { Path sourcePath = source.toPath();
copySymlink(source.toPath(), target.toPath(), totalItems, currentItem, callback); System.out.println("[DEBUG] Processing item: " + sourcePath + " (isLink: " + Files.isSymbolicLink(sourcePath) + ")");
if (Files.isSymbolicLink(sourcePath)) {
SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse);
System.out.println("[DEBUG] Symlink response for " + source.getName() + ": " + sRes);
if (sRes == SymlinkResponse.CANCEL) {
return;
}
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0] += countItems(sourcePath);
break;
}
// If FOLLOW, sRes will be FOLLOW or FOLLOW_ALL
if (Files.exists(sourcePath)) {
if (Files.isDirectory(sourcePath)) {
System.out.println("[DEBUG] Following symlink as directory: " + sourcePath);
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Count the symlink itself as processed to match countItems prediction
currentItem[0]++;
} else {
System.out.println("[DEBUG] Following symlink as file: " + sourcePath);
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
System.out.println("[DEBUG] Broken link encountered, copying as link: " + sourcePath);
copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
break;
} else if (source.isDirectory()) { } else if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse); System.out.println("[DEBUG] Copying directory: " + sourcePath);
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Directory itself counted in countItems but copyDirectory skips increment for root.
// So we increment here.
currentItem[0]++;
} else { } else {
copyFileWithProgress(source.toPath(), target.toPath(), totalItems, currentItem, callback); System.out.println("[DEBUG] Copying file: " + sourcePath);
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
} }
break; break;
} catch (IOException e) { } catch (IOException e) {
System.out.println("[DEBUG] Error processing " + source.getName() + ": " + e.getMessage());
if (callback != null) { if (callback != null) {
ErrorResponse res = callback.onError(source, e); ErrorResponse res = callback.onError(source, e);
if (res == ErrorResponse.ABORT) throw e; if (res == ErrorResponse.ABORT) throw e;
@ -86,6 +131,15 @@ public class FileOperations {
} }
} }
private static SymlinkResponse getSymlinkResponse(File source, ProgressCallback callback, SymlinkResponse[] globalSymlinkResponse) {
if (globalSymlinkResponse[0] != null) return globalSymlinkResponse[0];
if (callback == null) return SymlinkResponse.IGNORE;
SymlinkResponse res = callback.confirmSymlink(source);
if (res == SymlinkResponse.FOLLOW_ALL) globalSymlinkResponse[0] = SymlinkResponse.FOLLOW_ALL;
if (res == SymlinkResponse.IGNORE_ALL) globalSymlinkResponse[0] = SymlinkResponse.IGNORE_ALL;
return res;
}
private static long calculateTotalItems(List<FileItem> items) { private static long calculateTotalItems(List<FileItem> items) {
long total = 0; long total = 0;
for (FileItem item : items) { for (FileItem item : items) {
@ -154,79 +208,114 @@ public class FileOperations {
} }
} }
private static void copyDirectory(Path source, Path target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse) throws IOException { private static void copyDirectory(Path source, Path target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse, final SymlinkResponse[] globalSymlinkResponse, Set<Path> visitedPaths) throws IOException {
Files.walkFileTree(source, new SimpleFileVisitor<Path>() { final Path effectiveSource = Files.isSymbolicLink(source) ? source.toRealPath() : source.toAbsolutePath().normalize();
@Override final Path finalTarget = target.toAbsolutePath().normalize();
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir)); if (visitedPaths == null) visitedPaths = new HashSet<>();
if (Files.exists(targetDir) && !Files.isDirectory(targetDir)) { if (!visitedPaths.add(effectiveSource)) {
Files.delete(targetDir); // Cycle detected, skip this directory
} return;
while (true) { }
try {
Files.createDirectories(targetDir); try {
if (!dir.equals(source)) { final Set<Path> finalVisitedPaths = visitedPaths;
currentItem[0]++; Files.walkFileTree(effectiveSource, new SimpleFileVisitor<Path>() {
if (callback != null) { @Override
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString()); public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = finalTarget.resolve(effectiveSource.relativize(dir.toAbsolutePath().normalize()));
if (Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(targetDir, LinkOption.NOFOLLOW_LINKS)) {
Files.delete(targetDir);
}
while (true) {
try {
Files.createDirectories(targetDir);
if (!dir.toAbsolutePath().normalize().equals(effectiveSource)) {
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
}
} }
}
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.SKIP_SUBTREE;
}
throw e;
}
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
Path targetFile = target.resolve(source.relativize(file));
if (Files.exists(targetFile)) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
OverwriteResponse res = callback.confirmOverwrite(file.toFile(), targetFile.toFile());
if (res == OverwriteResponse.CANCEL) return FileVisitResult.TERMINATE;
if (res == OverwriteResponse.NO_TO_ALL) {
globalResponse[0] = OverwriteResponse.NO_TO_ALL;
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.SKIP_SUBTREE;
}
throw e;
} }
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
}
// If we are here, we are overwriting. If target is a directory, delete it.
if (Files.isDirectory(targetFile)) {
deleteDirectoryInternal(targetFile);
} }
} }
while (true) { @Override
try { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (Files.isSymbolicLink(file)) { if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
copySymlink(file, targetFile, totalItems, currentItem, callback); Path targetFile = finalTarget.resolve(effectiveSource.relativize(file.toAbsolutePath().normalize()));
System.out.println("[DEBUG] Recursive visit (file): " + file + " (isLink: " + attrs.isSymbolicLink() + ")");
if (Files.exists(targetFile, LinkOption.NOFOLLOW_LINKS)) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
OverwriteResponse res = callback.confirmOverwrite(file.toFile(), targetFile.toFile());
if (res == OverwriteResponse.CANCEL) return FileVisitResult.TERMINATE;
if (res == OverwriteResponse.NO_TO_ALL) {
globalResponse[0] = OverwriteResponse.NO_TO_ALL;
return FileVisitResult.CONTINUE;
}
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
}
// Handle directory vs file clash
if (Files.isDirectory(targetFile, LinkOption.NOFOLLOW_LINKS)) {
deleteDirectoryInternal(targetFile);
} else { } else {
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback); Files.delete(targetFile);
} }
return FileVisitResult.CONTINUE; }
} catch (IOException e) {
if (callback != null) { while (true) {
ErrorResponse res = callback.onError(file.toFile(), e); try {
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE; if (attrs.isSymbolicLink()) {
if (res == ErrorResponse.RETRY) continue; SymlinkResponse sRes = getSymlinkResponse(file.toFile(), callback, globalSymlinkResponse);
if (sRes == SymlinkResponse.CANCEL) return FileVisitResult.TERMINATE;
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0]++;
return FileVisitResult.CONTINUE;
}
// FOLLOW or FOLLOW_ALL
if (Files.exists(file)) {
if (Files.isDirectory(file)) {
copyDirectory(file, targetFile, totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, finalVisitedPaths);
// Count the symlink itself as processed to match countItems prediction
currentItem[0]++;
} else {
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
copySymlink(file, targetFile, totalItems, currentItem, callback);
}
} else {
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
}
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
} }
throw e;
} }
} }
} });
}); } finally {
visitedPaths.remove(effectiveSource);
}
} }
private static void deleteDirectoryInternal(Path path) throws IOException { private static void deleteDirectoryInternal(Path path) throws IOException {
@ -257,9 +346,19 @@ public class FileOperations {
} }
List<FileItem> cleanedItems = cleanDuplicateItems(items); List<FileItem> cleanedItems = cleanDuplicateItems(items);
// Sort items so symlinks are last
cleanedItems.sort((a, b) -> {
boolean aSym = Files.isSymbolicLink(a.getFile().toPath());
boolean bSym = Files.isSymbolicLink(b.getFile().toPath());
if (aSym && !bSym) return 1;
if (!aSym && bSym) return -1;
return 0;
});
long totalItems = calculateTotalItems(cleanedItems); long totalItems = calculateTotalItems(cleanedItems);
long[] currentItem = {0}; long[] currentItem = {0};
final OverwriteResponse[] globalResponse = {null}; final OverwriteResponse[] globalResponse = {null};
final SymlinkResponse[] globalSymlinkResponse = {null};
for (FileItem item : cleanedItems) { for (FileItem item : cleanedItems) {
if (callback != null && callback.isCancelled()) break; if (callback != null && callback.isCancelled()) break;
@ -295,7 +394,32 @@ public class FileOperations {
long itemCount = countItems(source.toPath()); long itemCount = countItems(source.toPath());
while (true) { while (true) {
try { try {
Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); Path sourcePath = source.toPath();
if (Files.isSymbolicLink(sourcePath)) {
SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse);
if (sRes == SymlinkResponse.CANCEL) return;
if (sRes == SymlinkResponse.IGNORE || sRes == SymlinkResponse.IGNORE_ALL) {
currentItem[0] += itemCount;
break;
}
if (sRes == SymlinkResponse.FOLLOW || sRes == SymlinkResponse.FOLLOW_ALL) {
if (Files.exists(sourcePath)) {
if (Files.isDirectory(sourcePath)) {
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
currentItem[0]++;
} else {
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
} else {
// Link is broken, follow is impossible, copy as link
copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback);
}
Files.delete(sourcePath);
break;
}
}
Files.move(sourcePath, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
currentItem[0] += itemCount; currentItem[0] += itemCount;
if (callback != null) { if (callback != null) {
callback.onProgress(currentItem[0], totalItems, source.getName()); callback.onProgress(currentItem[0], totalItems, source.getName());
@ -305,7 +429,7 @@ public class FileOperations {
// Fallback for cross-device moves (e.g., to network/Samba mounts) // Fallback for cross-device moves (e.g., to network/Samba mounts)
try { try {
if (source.isDirectory()) { if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse); copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
currentItem[0]++; // For the directory itself which copyDirectory skips currentItem[0]++; // For the directory itself which copyDirectory skips
deleteDirectoryInternal(source.toPath()); deleteDirectoryInternal(source.toPath());
} else { } else {
@ -398,19 +522,15 @@ public class FileOperations {
private static boolean isSubfolder(File parent, File child) { private static boolean isSubfolder(File parent, File child) {
if (parent == null || child == null) return false; if (parent == null || child == null) return false;
try { String parentPath = parent.getAbsolutePath();
String parentPath = parent.getCanonicalPath(); String childPath = child.getAbsolutePath();
String childPath = child.getCanonicalPath();
if (parentPath.equals(childPath)) return true; if (parentPath.equals(childPath)) return true;
if (!parentPath.endsWith(File.separator)) { if (!parentPath.endsWith(File.separator)) {
parentPath += File.separator; parentPath += File.separator;
}
return childPath.startsWith(parentPath);
} catch (IOException e) {
return child.getAbsolutePath().startsWith(parent.getAbsolutePath());
} }
return childPath.startsWith(parentPath);
} }
/** /**
@ -911,6 +1031,10 @@ public class FileOperations {
YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL
} }
public enum SymlinkResponse {
FOLLOW, IGNORE, FOLLOW_ALL, IGNORE_ALL, CANCEL
}
public enum ErrorResponse { public enum ErrorResponse {
SKIP, RETRY, ABORT SKIP, RETRY, ABORT
} }
@ -920,6 +1044,7 @@ public class FileOperations {
default void onFileProgress(long current, long total) {} default void onFileProgress(long current, long total) {}
default boolean isCancelled() { return false; } default boolean isCancelled() { return false; }
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; } default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; } default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
} }

View File

@ -42,6 +42,18 @@ public class FilePanel extends JPanel {
if (tab != null) tab.selectByWildcard(pattern); if (tab != null) tab.selectByWildcard(pattern);
} }
/** Invert selection in the current tab. */
public void invertSelection() {
FilePanelTab tab = getCurrentTab();
if (tab != null) tab.invertSelection();
}
/** Unselect all files in the current tab. */
public void unselectAll() {
FilePanelTab tab = getCurrentTab();
if (tab != null) tab.unselectAll();
}
private Runnable switchPanelCallback; private Runnable switchPanelCallback;
public void setSwitchPanelCallback(Runnable cb) { public void setSwitchPanelCallback(Runnable cb) {

View File

@ -1774,6 +1774,32 @@ public class FilePanelTab extends JPanel {
selectItemByName(name, requestFocus); selectItemByName(name, requestFocus);
} }
public void invertSelection() {
if (tableModel == null || tableModel.items == null) return;
for (FileItem item : tableModel.items) {
if (!item.getName().equals("..")) {
item.setMarked(!item.isMarked());
}
}
fileTable.repaint();
updateStatus();
}
public void unselectAll() {
if (tableModel == null || tableModel.items == null) return;
boolean changed = false;
for (FileItem item : tableModel.items) {
if (item.isMarked()) {
item.setMarked(false);
changed = true;
}
}
if (changed) {
fileTable.repaint();
updateStatus();
}
}
public void toggleSelectionAndMoveDown() { public void toggleSelectionAndMoveDown() {
int selectedRow = fileTable.getSelectedRow(); int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) { if (selectedRow >= 0) {

View File

@ -809,8 +809,14 @@ public class MainWindow extends JFrame {
searchItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F7, InputEvent.ALT_DOWN_MASK)); searchItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F7, InputEvent.ALT_DOWN_MASK));
searchItem.addActionListener(e -> showSearchDialog()); searchItem.addActionListener(e -> showSearchDialog());
JMenuItem selectAllItem = new JMenuItem("Invert Selection");
selectAllItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK));
selectAllItem.addActionListener(e -> {
if (activePanel != null) activePanel.invertSelection();
});
JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard..."); JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard...");
selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK)); selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0));
selectWildcardItem.addActionListener(e -> showWildcardSelectDialog()); selectWildcardItem.addActionListener(e -> showWildcardSelectDialog());
JMenuItem refreshItem = new JMenuItem("Refresh"); JMenuItem refreshItem = new JMenuItem("Refresh");
@ -826,6 +832,7 @@ public class MainWindow extends JFrame {
exitItem.addActionListener(e -> saveConfigAndExit()); exitItem.addActionListener(e -> saveConfigAndExit());
fileMenu.add(searchItem); fileMenu.add(searchItem);
fileMenu.add(selectAllItem);
fileMenu.add(selectWildcardItem); fileMenu.add(selectWildcardItem);
fileMenu.add(refreshItem); fileMenu.add(refreshItem);
fileMenu.add(queueItem); fileMenu.add(queueItem);
@ -1355,11 +1362,33 @@ public class MainWindow extends JFrame {
} }
}); });
// Wildcard selection (Ctrl+A and +) // Selection (Ctrl+A for Invert, + for Wildcard)
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK), "wildcardSelect"); .put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK), "invertSelection");
table.getActionMap().put("invertSelection", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (activePanel != null) {
activePanel.invertSelection();
}
}
});
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "unselectAll");
table.getActionMap().put("unselectAll", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (activePanel != null) {
activePanel.unselectAll();
}
}
});
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke('+'), "wildcardSelect"); .put(KeyStroke.getKeyStroke('+'), "wildcardSelect");
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "wildcardSelect");
table.getActionMap().put("wildcardSelect", new AbstractAction() { table.getActionMap().put("wildcardSelect", new AbstractAction() {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
@ -2306,6 +2335,44 @@ public class MainWindow extends JFrame {
return result[0]; return result[0];
} }
@Override
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];
try {
SwingUtilities.invokeAndWait(() -> {
String message = String.format(
"Symbolic link encountered: %s\n\n" +
"What do you want to do?",
symlink.getAbsolutePath()
);
Object[] options = {"Follow", "Follow All", "Ignore", "Ignore All", "Cancel"};
int n = JOptionPane.showOptionDialog(progressDialog,
message,
"Symlink Encountered",
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0]);
switch (n) {
case 0: result[0] = FileOperations.SymlinkResponse.FOLLOW; break;
case 1: result[0] = FileOperations.SymlinkResponse.FOLLOW_ALL; break;
case 2: result[0] = FileOperations.SymlinkResponse.IGNORE; break;
case 3: result[0] = FileOperations.SymlinkResponse.IGNORE_ALL; break;
default:
result[0] = FileOperations.SymlinkResponse.CANCEL;
progressDialog.cancel();
break;
}
});
} catch (Exception e) {
result[0] = FileOperations.SymlinkResponse.CANCEL;
}
return result[0];
}
@Override @Override
public FileOperations.ErrorResponse onError(File file, Exception e) { public FileOperations.ErrorResponse onError(File file, Exception e) {
final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1]; final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1];