fixed panel scrolling

This commit is contained in:
Radek Davidek 2026-06-09 15:11:07 +02:00
parent caac494639
commit 7f708c3337

View File

@ -28,8 +28,10 @@ import java.io.InputStreamReader;
import java.io.FileReader; import java.io.FileReader;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -90,6 +92,7 @@ public class FilePanelTab extends JPanel {
private long archiveExtractTime = 0; // Track time of extraction to detect changes private long archiveExtractTime = 0; // Track time of extraction to detect changes
private File archiveReturnDirectory = null; private File archiveReturnDirectory = null;
private Point archiveReturnViewPosition = null; private Point archiveReturnViewPosition = null;
private final Deque<ReturnNavigationState> navigationReturnStates = new ArrayDeque<>();
private boolean inlineRenameActive = false; private boolean inlineRenameActive = false;
private boolean refreshDeferredWhileInlineRename = false; private boolean refreshDeferredWhileInlineRename = false;
private boolean deferredRefreshRequestFocus = false; private boolean deferredRefreshRequestFocus = false;
@ -105,6 +108,20 @@ public class FilePanelTab extends JPanel {
private FtpProfile ftpProfile; private FtpProfile ftpProfile;
private String ftpCurrentPath; private String ftpCurrentPath;
private static final class ReturnNavigationState {
private final File parentDirectory;
private final String itemName;
private final Point itemOffsetInView;
private final Point fallbackViewPosition;
private ReturnNavigationState(File parentDirectory, String itemName, Point itemOffsetInView, Point fallbackViewPosition) {
this.parentDirectory = parentDirectory;
this.itemName = itemName;
this.itemOffsetInView = itemOffsetInView;
this.fallbackViewPosition = fallbackViewPosition;
}
}
public FilePanelTab(String initialPath) { public FilePanelTab(String initialPath) {
this(initialPath, true); this(initialPath, true);
} }
@ -1520,6 +1537,125 @@ public class FilePanelTab extends JPanel {
} }
} }
private ReturnNavigationState rememberReturnStateForItem(FileItem item, int row, int column) {
if (item == null || item.getName() == null || currentDirectory == null || fileTable == null || row < 0) {
return null;
}
Rectangle cell = fileTable.getCellRect(row, Math.max(0, column), true);
Rectangle visible = fileTable.getVisibleRect();
Point offset = new Point(cell.x - visible.x, cell.y - visible.y);
Point fallback = visible.getLocation();
ReturnNavigationState state = new ReturnNavigationState(
currentDirectory,
item.getName(),
offset,
fallback
);
navigationReturnStates.push(state);
return state;
}
private void discardReturnState(ReturnNavigationState state) {
if (state == null) {
return;
}
navigationReturnStates.removeFirstOccurrence(state);
}
private ReturnNavigationState consumeReturnState(File parentDirectory, String itemName) {
if (parentDirectory == null || itemName == null) {
return null;
}
java.util.Iterator<ReturnNavigationState> it = navigationReturnStates.iterator();
while (it.hasNext()) {
ReturnNavigationState state = it.next();
if (parentDirectory.equals(state.parentDirectory) && itemName.equals(state.itemName)) {
it.remove();
return state;
}
}
return null;
}
private void restoreReturnState(ReturnNavigationState state, boolean requestFocus) {
if (state == null || fileTable == null || currentDirectory == null) {
return;
}
if (!currentDirectory.equals(state.parentDirectory)) {
return;
}
for (int i = 0; i < tableModel.items.size(); i++) {
FileItem item = tableModel.items.get(i);
if (item == null || !state.itemName.equalsIgnoreCase(item.getName())) {
continue;
}
if (viewMode == ViewMode.BRIEF) {
int column = i / tableModel.briefRowsPerColumn;
int row = i % tableModel.briefRowsPerColumn;
selectBriefItemWithoutAutoScroll(row, column);
restoreViewportForCell(row, column, state);
final int finalRow = row;
final int finalColumn = column;
SwingUtilities.invokeLater(() -> restoreViewportForCell(finalRow, finalColumn, state));
} else {
selectFullRowWithoutAutoScroll(i);
restoreViewportForCell(i, 0, state);
final int finalRow = i;
SwingUtilities.invokeLater(() -> restoreViewportForCell(finalRow, 0, state));
}
fileTable.repaint();
if (requestFocus) {
fileTable.requestFocusInWindow();
}
updateStatus();
return;
}
}
private void restoreViewportForCell(int row, int column, ReturnNavigationState state) {
if (fileTable == null || state == null) {
return;
}
Rectangle cell = fileTable.getCellRect(row, Math.max(0, column), true);
Container parent = fileTable.getParent();
if (parent instanceof JViewport viewport) {
if (state.fallbackViewPosition != null) {
viewport.setViewPosition(clampViewPosition(viewport, new Point(state.fallbackViewPosition)));
}
Rectangle visibleAfterFallback = viewport.getViewRect();
if (visibleAfterFallback.intersects(cell)) {
return;
}
Point target = (state.itemOffsetInView != null)
? new Point(cell.x - state.itemOffsetInView.x, cell.y - state.itemOffsetInView.y)
: new Point(state.fallbackViewPosition != null ? state.fallbackViewPosition : new Point());
viewport.setViewPosition(clampViewPosition(viewport, target));
} else if (state.fallbackViewPosition != null) {
fileTable.scrollRectToVisible(new Rectangle(state.fallbackViewPosition.x, state.fallbackViewPosition.y, 1, 1));
}
}
private Point clampViewPosition(JViewport viewport, Point target) {
Dimension pref = fileTable.getPreferredSize();
int contentW = Math.max(fileTable.getWidth(), pref != null ? pref.width : 0);
int contentH = Math.max(fileTable.getHeight(), pref != null ? pref.height : 0);
int maxX = Math.max(0, contentW - viewport.getWidth());
int maxY = Math.max(0, contentH - viewport.getHeight());
return new Point(
Math.max(0, Math.min(target.x, maxX)),
Math.max(0, Math.min(target.y, maxY))
);
}
private void scrollBriefCellToColumnStart(int row, int column) { private void scrollBriefCellToColumnStart(int row, int column) {
if (fileTable == null || viewMode != ViewMode.BRIEF || row < 0 || column < 0 if (fileTable == null || viewMode != ViewMode.BRIEF || row < 0 || column < 0
|| column >= fileTable.getColumnModel().getColumnCount()) { || column >= fileTable.getColumnModel().getColumnCount()) {
@ -1659,6 +1795,8 @@ public class FilePanelTab extends JPanel {
} else { } else {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
alignTableItemsToViewportStart(); alignTableItemsToViewportStart();
fileTable.revalidate();
fileTable.repaint();
if (autoSelectFirst && fileTable.getRowCount() > 0) { if (autoSelectFirst && fileTable.getRowCount() > 0) {
int startIndex = 0; int startIndex = 0;
fileTable.setRowSelectionInterval(startIndex, startIndex); fileTable.setRowSelectionInterval(startIndex, startIndex);
@ -2168,6 +2306,7 @@ public class FilePanelTab extends JPanel {
if (item.getName().equals("..")) { if (item.getName().equals("..")) {
navigateUp(); navigateUp();
} else if (FileOperations.canOpenAsArchive(item.getFile())) { } else if (FileOperations.canOpenAsArchive(item.getFile())) {
ReturnNavigationState returnState = rememberReturnStateForItem(item, selectedRow, viewMode == ViewMode.BRIEF ? briefCurrentColumn : 0);
rememberArchiveReturnState(item.getFile()); rememberArchiveReturnState(item.getFile());
final File archiveFile = item.getFile(); final File archiveFile = item.getFile();
@ -2195,11 +2334,13 @@ public class FilePanelTab extends JPanel {
loadDirectory(temp.toFile()); loadDirectory(temp.toFile());
}, },
error -> { error -> {
discardReturnState(returnState);
clearArchiveReturnState(); clearArchiveReturnState();
JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE);
} }
); );
} else if (item.isDirectory()) { } else if (item.isDirectory()) {
rememberReturnStateForItem(item, selectedRow, viewMode == ViewMode.BRIEF ? briefCurrentColumn : 0);
loadDirectory(item.getFile()); loadDirectory(item.getFile());
} else if (item.getFile() != null && item.getFile().isFile()) { } else if (item.getFile() != null && item.getFile().isFile()) {
openFileNative(item.getFile()); openFileNative(item.getFile());
@ -2715,6 +2856,7 @@ public class FilePanelTab extends JPanel {
if (item.getName().equals("..")) { if (item.getName().equals("..")) {
navigateUp(); navigateUp();
} else if (FileOperations.canOpenAsArchive(item.getFile())) { } else if (FileOperations.canOpenAsArchive(item.getFile())) {
ReturnNavigationState returnState = rememberReturnStateForItem(item, row, viewMode == ViewMode.BRIEF ? col : 0);
rememberArchiveReturnState(item.getFile()); rememberArchiveReturnState(item.getFile());
final File archiveFile = item.getFile(); final File archiveFile = item.getFile();
@ -2732,11 +2874,13 @@ public class FilePanelTab extends JPanel {
loadDirectory(temp.toFile(), true, true); loadDirectory(temp.toFile(), true, true);
}, },
error -> { error -> {
discardReturnState(returnState);
clearArchiveReturnState(); clearArchiveReturnState();
JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE);
} }
); );
} else if (item.isDirectory()) { } else if (item.isDirectory()) {
rememberReturnStateForItem(item, row, viewMode == ViewMode.BRIEF ? col : 0);
loadDirectory(item.getFile()); loadDirectory(item.getFile());
} else if (item.getFile() != null && item.getFile().isFile()) { } else if (item.getFile() != null && item.getFile().isFile()) {
openFileNative(item.getFile()); openFileNative(item.getFile());
@ -3143,6 +3287,7 @@ public class FilePanelTab extends JPanel {
File parent = currentArchiveSourceFile.getParentFile(); File parent = currentArchiveSourceFile.getParentFile();
if (parent != null) { if (parent != null) {
String archiveName = currentArchiveSourceFile.getName(); String archiveName = currentArchiveSourceFile.getName();
ReturnNavigationState returnState = consumeReturnState(parent, archiveName);
// Save any changes made to the archive before leaving (only if modified) // Save any changes made to the archive before leaving (only if modified)
if (FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { if (FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) {
@ -3159,13 +3304,9 @@ public class FilePanelTab extends JPanel {
deleteTempDirRecursively(currentArchiveTempDir); deleteTempDirRecursively(currentArchiveTempDir);
clearOpenedArchiveSession(); clearOpenedArchiveSession();
loadDirectory(parent, false, true, () -> { loadDirectory(parent, false, true, () -> {
// Restore original viewport position and keep focus on archive item. restoreReturnState(returnState, true);
// In BRIEF mode some selection listeners may still run later, so apply once more on EDT tail.
restoreArchiveReturnPosition();
selectItemByName(archiveName, true, false);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
restoreArchiveReturnPosition(); restoreReturnState(returnState, true);
selectItemByName(archiveName, true, false);
clearArchiveReturnState(); clearArchiveReturnState();
}); });
}); });
@ -3178,10 +3319,18 @@ public class FilePanelTab extends JPanel {
File parent = currentDirectory.getParentFile(); File parent = currentDirectory.getParentFile();
if (parent != null) { if (parent != null) {
String previousDirName = currentDirectory.getName(); String previousDirName = currentDirectory.getName();
loadDirectory(parent, false); ReturnNavigationState returnState = consumeReturnState(parent, previousDirName);
loadDirectory(parent, false, true, () -> {
if (returnState != null) {
restoreReturnState(returnState, true);
} else {
selectItemByName(previousDirName);
}
});
// Select the directory we just left if (returnState != null) {
SwingUtilities.invokeLater(() -> selectItemByName(previousDirName)); SwingUtilities.invokeLater(() -> restoreReturnState(returnState, true));
}
} }
} }