From 7f708c3337220dd8b34136503fe492884f581be1 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Tue, 9 Jun 2026 15:11:07 +0200 Subject: [PATCH] fixed panel scrolling --- .../cz/kamma/kfmanager/ui/FilePanelTab.java | 167 +++++++++++++++++- 1 file changed, 158 insertions(+), 9 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index b6d3baf..6a12e89 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -28,8 +28,10 @@ import java.io.InputStreamReader; import java.io.FileReader; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Comparator; +import java.util.Deque; import java.util.Date; import java.util.HashMap; 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 File archiveReturnDirectory = null; private Point archiveReturnViewPosition = null; + private final Deque navigationReturnStates = new ArrayDeque<>(); private boolean inlineRenameActive = false; private boolean refreshDeferredWhileInlineRename = false; private boolean deferredRefreshRequestFocus = false; @@ -105,6 +108,20 @@ public class FilePanelTab extends JPanel { private FtpProfile ftpProfile; 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) { 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 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) { if (fileTable == null || viewMode != ViewMode.BRIEF || row < 0 || column < 0 || column >= fileTable.getColumnModel().getColumnCount()) { @@ -1659,6 +1795,8 @@ public class FilePanelTab extends JPanel { } else { SwingUtilities.invokeLater(() -> { alignTableItemsToViewportStart(); + fileTable.revalidate(); + fileTable.repaint(); if (autoSelectFirst && fileTable.getRowCount() > 0) { int startIndex = 0; fileTable.setRowSelectionInterval(startIndex, startIndex); @@ -2168,6 +2306,7 @@ public class FilePanelTab extends JPanel { if (item.getName().equals("..")) { navigateUp(); } else if (FileOperations.canOpenAsArchive(item.getFile())) { + ReturnNavigationState returnState = rememberReturnStateForItem(item, selectedRow, viewMode == ViewMode.BRIEF ? briefCurrentColumn : 0); rememberArchiveReturnState(item.getFile()); final File archiveFile = item.getFile(); @@ -2195,11 +2334,13 @@ public class FilePanelTab extends JPanel { loadDirectory(temp.toFile()); }, error -> { + discardReturnState(returnState); clearArchiveReturnState(); JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE); } ); } else if (item.isDirectory()) { + rememberReturnStateForItem(item, selectedRow, viewMode == ViewMode.BRIEF ? briefCurrentColumn : 0); loadDirectory(item.getFile()); } else if (item.getFile() != null && item.getFile().isFile()) { openFileNative(item.getFile()); @@ -2715,6 +2856,7 @@ public class FilePanelTab extends JPanel { if (item.getName().equals("..")) { navigateUp(); } else if (FileOperations.canOpenAsArchive(item.getFile())) { + ReturnNavigationState returnState = rememberReturnStateForItem(item, row, viewMode == ViewMode.BRIEF ? col : 0); rememberArchiveReturnState(item.getFile()); final File archiveFile = item.getFile(); @@ -2732,11 +2874,13 @@ public class FilePanelTab extends JPanel { loadDirectory(temp.toFile(), true, true); }, error -> { + discardReturnState(returnState); clearArchiveReturnState(); JOptionPane.showMessageDialog(FilePanelTab.this, error, "Archive Error", JOptionPane.ERROR_MESSAGE); } ); } else if (item.isDirectory()) { + rememberReturnStateForItem(item, row, viewMode == ViewMode.BRIEF ? col : 0); loadDirectory(item.getFile()); } else if (item.getFile() != null && item.getFile().isFile()) { openFileNative(item.getFile()); @@ -3143,6 +3287,7 @@ public class FilePanelTab extends JPanel { File parent = currentArchiveSourceFile.getParentFile(); if (parent != null) { String archiveName = currentArchiveSourceFile.getName(); + ReturnNavigationState returnState = consumeReturnState(parent, archiveName); // Save any changes made to the archive before leaving (only if modified) if (FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { @@ -3159,13 +3304,9 @@ public class FilePanelTab extends JPanel { deleteTempDirRecursively(currentArchiveTempDir); clearOpenedArchiveSession(); loadDirectory(parent, false, true, () -> { - // Restore original viewport position and keep focus on archive item. - // In BRIEF mode some selection listeners may still run later, so apply once more on EDT tail. - restoreArchiveReturnPosition(); - selectItemByName(archiveName, true, false); + restoreReturnState(returnState, true); SwingUtilities.invokeLater(() -> { - restoreArchiveReturnPosition(); - selectItemByName(archiveName, true, false); + restoreReturnState(returnState, true); clearArchiveReturnState(); }); }); @@ -3178,10 +3319,18 @@ public class FilePanelTab extends JPanel { File parent = currentDirectory.getParentFile(); if (parent != null) { 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 - SwingUtilities.invokeLater(() -> selectItemByName(previousDirName)); + if (returnState != null) { + SwingUtilities.invokeLater(() -> restoreReturnState(returnState, true)); + } } }