fixed follow link, selection improved
This commit is contained in:
parent
03472ae2e4
commit
a731023ed0
@ -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?
|
||||||
|
|||||||
@ -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) + ")");
|
||||||
} else if (source.isDirectory()) {
|
if (Files.isSymbolicLink(sourcePath)) {
|
||||||
copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse);
|
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 {
|
} else {
|
||||||
copyFileWithProgress(source.toPath(), target.toPath(), totalItems, currentItem, callback);
|
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()) {
|
||||||
|
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 {
|
||||||
|
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,18 +208,29 @@ 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();
|
||||||
|
final Path finalTarget = target.toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
if (visitedPaths == null) visitedPaths = new HashSet<>();
|
||||||
|
if (!visitedPaths.add(effectiveSource)) {
|
||||||
|
// Cycle detected, skip this directory
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Set<Path> finalVisitedPaths = visitedPaths;
|
||||||
|
Files.walkFileTree(effectiveSource, new SimpleFileVisitor<Path>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
Path targetDir = target.resolve(source.relativize(dir));
|
Path targetDir = finalTarget.resolve(effectiveSource.relativize(dir.toAbsolutePath().normalize()));
|
||||||
if (Files.exists(targetDir) && !Files.isDirectory(targetDir)) {
|
if (Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(targetDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
Files.delete(targetDir);
|
Files.delete(targetDir);
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(targetDir);
|
Files.createDirectories(targetDir);
|
||||||
if (!dir.equals(source)) {
|
if (!dir.toAbsolutePath().normalize().equals(effectiveSource)) {
|
||||||
currentItem[0]++;
|
currentItem[0]++;
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
|
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
|
||||||
@ -187,9 +252,10 @@ public class FileOperations {
|
|||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
|
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
|
||||||
Path targetFile = target.resolve(source.relativize(file));
|
Path targetFile = finalTarget.resolve(effectiveSource.relativize(file.toAbsolutePath().normalize()));
|
||||||
|
System.out.println("[DEBUG] Recursive visit (file): " + file + " (isLink: " + attrs.isSymbolicLink() + ")");
|
||||||
|
|
||||||
if (Files.exists(targetFile)) {
|
if (Files.exists(targetFile, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
|
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
|
||||||
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
|
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
|
||||||
OverwriteResponse res = callback.confirmOverwrite(file.toFile(), targetFile.toFile());
|
OverwriteResponse res = callback.confirmOverwrite(file.toFile(), targetFile.toFile());
|
||||||
@ -201,16 +267,36 @@ public class FileOperations {
|
|||||||
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
|
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
|
||||||
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
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.
|
// Handle directory vs file clash
|
||||||
if (Files.isDirectory(targetFile)) {
|
if (Files.isDirectory(targetFile, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
deleteDirectoryInternal(targetFile);
|
deleteDirectoryInternal(targetFile);
|
||||||
|
} else {
|
||||||
|
Files.delete(targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (Files.isSymbolicLink(file)) {
|
if (attrs.isSymbolicLink()) {
|
||||||
|
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);
|
copySymlink(file, targetFile, totalItems, currentItem, callback);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
|
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
|
||||||
}
|
}
|
||||||
@ -227,6 +313,9 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} 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,9 +522,8 @@ 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;
|
||||||
|
|
||||||
@ -408,9 +531,6 @@ public class FileOperations {
|
|||||||
parentPath += File.separator;
|
parentPath += File.separator;
|
||||||
}
|
}
|
||||||
return childPath.startsWith(parentPath);
|
return childPath.startsWith(parentPath);
|
||||||
} catch (IOException e) {
|
|
||||||
return child.getAbsolutePath().startsWith(parent.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user