diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index a51e6ce..35c61fd 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -725,9 +725,23 @@ public class FilePanelTab extends JPanel { JScrollBar hBar = sp.getHorizontalScrollBar(); if (hBar != null && hBar.isVisible()) { int rotation = e.getWheelRotation(); - // Scroll by a larger amount for significantly faster movement - int amount = rotation * hBar.getUnitIncrement() * 10; - hBar.setValue(hBar.getValue() + amount); + int colWidth = fileTable.getColumnModel().getColumn(0).getPreferredWidth(); + if (colWidth > 0) { + // Calculate current value and round to nearest column to ensure snapping + int currentVal = hBar.getValue(); + int currentCol = (int) Math.round((double) currentVal / colWidth); + + // Scroll by 2 columns per notch for speed but keep it aligned + int nextCol = currentCol + (rotation * 2); + int newVal = nextCol * colWidth; + + // Bounds checking + int maxVal = hBar.getMaximum() - hBar.getVisibleAmount(); + if (newVal < 0) newVal = 0; + if (newVal > maxVal) newVal = maxVal; + + hBar.setValue(newVal); + } } } e.consume(); @@ -2706,6 +2720,49 @@ public class FilePanelTab extends JPanel { return icon; } + private int calculateCurrentColumnWidth() { + if (tableModel.items == null || tableModel.items.isEmpty()) return 0; + + java.awt.FontMetrics fm = fileTable.getFontMetrics(fileTable.getFont()); + int maxTextWidth = 0; + int maxIconWidth = 0; + + for (FileItem item : tableModel.items) { + if (item == null) continue; + String name = item.getName(); + // Match truncation from renderer for BRIEF mode + int maxLen = persistedConfig != null ? persistedConfig.getBriefModeMaxNameLength() : 30; + if (name.length() > maxLen) { + int startLen = persistedConfig != null ? persistedConfig.getBriefModeStartLength() : 20; + int endLen = persistedConfig != null ? persistedConfig.getBriefModeEndLength() : 10; + String sep = persistedConfig != null ? persistedConfig.getBriefModeSeparator() : "..."; + if (startLen + endLen < name.length()) { + name = name.substring(0, startLen) + sep + name.substring(name.length() - endLen); + } + } + String display = name; + if (item.isDirectory()) { + display = "[" + display + "]"; + } + int w = fm.stringWidth(display); + if (w > maxTextWidth) maxTextWidth = w; + + Icon icon = getItemIcon(item); + if (icon != null) { + int iw = icon.getIconWidth(); + if (iw > maxIconWidth) maxIconWidth = iw; + } + } + + if (maxTextWidth == 0) { + maxTextWidth = fm.stringWidth("WWWWWWWWWW"); + } + + // Add icon width and padding (left/right + gap between icon and text) + int padding = 36; // reasonable extra space + return maxTextWidth + maxIconWidth + padding; + } + private void updateColumnWidths() { if (viewMode == ViewMode.FULL) { if (fileTable.getColumnModel().getColumnCount() == 3) { @@ -2730,51 +2787,32 @@ public class FilePanelTab extends JPanel { // Turn off auto-resize so preferred widths are honored and horizontal scrolling appears fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - // Compute the preferred column width based on the longest displayed - // name in the current directory (include brackets for directories) - // plus the widest icon width and some padding. - java.awt.FontMetrics fm = fileTable.getFontMetrics(fileTable.getFont()); - int maxTextWidth = 0; - int maxIconWidth = 0; - - for (FileItem item : tableModel.items) { - if (item == null) continue; - String name = item.getName(); - // Match truncation from renderer for BRIEF mode - int maxLen = persistedConfig != null ? persistedConfig.getBriefModeMaxNameLength() : 30; - if (name.length() > maxLen) { - int startLen = persistedConfig != null ? persistedConfig.getBriefModeStartLength() : 20; - int endLen = persistedConfig != null ? persistedConfig.getBriefModeEndLength() : 10; - String sep = persistedConfig != null ? persistedConfig.getBriefModeSeparator() : "..."; - if (startLen + endLen < name.length()) { - name = name.substring(0, startLen) + sep + name.substring(name.length() - endLen); - } - } - String display = name; - if (item.isDirectory()) { - display = "[" + display + "]"; - } - int w = fm.stringWidth(display); - if (w > maxTextWidth) maxTextWidth = w; - - Icon icon = getItemIcon(item); - if (icon != null) { - int iw = icon.getIconWidth(); - if (iw > maxIconWidth) maxIconWidth = iw; - } - } - - if (maxTextWidth == 0) { - maxTextWidth = fm.stringWidth("WWWWWWWWWW"); - } - - // Add icon width and padding (left/right + gap between icon and text) - int padding = 36; // reasonable extra space - int columnWidth = maxTextWidth + maxIconWidth + padding; + int columnWidth = calculateCurrentColumnWidth(); + int viewportWidth = fileTable.getParent().getWidth(); for (int i = 0; i < columnCount; i++) { fileTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth); } + + // Adjust the very last column to make the total table width such that + // the maximum scroll position is a multiple of columnWidth. + if (tableModel.extraColumns > 0 && columnWidth > 0 && viewportWidth > 0) { + int actualDataWidth = tableModel.briefColumns * columnWidth; + int k = (int) Math.ceil((double) (actualDataWidth - viewportWidth) / columnWidth); + int targetTotalWidth = k * columnWidth + viewportWidth; + + int widthOfOthers = (columnCount - 1) * columnWidth; + int lastColWidth = targetTotalWidth - widthOfOthers; + if (lastColWidth > 0) { + fileTable.getColumnModel().getColumn(columnCount - 1).setPreferredWidth(lastColWidth); + } + } + + // Ensure horizontal scrollbar snaps to column widths + JScrollPane sp = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, fileTable); + if (sp != null) { + sp.getHorizontalScrollBar().setUnitIncrement(columnWidth); + } } } @@ -3191,6 +3229,7 @@ public class FilePanelTab extends JPanel { private List items = new ArrayList<>(); private String[] columnNames = {"Name", "Size", "Date"}; public int briefColumns = 1; + public int extraColumns = 0; public int briefRowsPerColumn = 10; public void setItems(List items) { @@ -3211,6 +3250,7 @@ public class FilePanelTab extends JPanel { public void calculateBriefLayout() { if (items.isEmpty()) { briefColumns = 1; + extraColumns = 0; briefRowsPerColumn = 1; return; } @@ -3221,11 +3261,36 @@ public class FilePanelTab extends JPanel { if (availableHeight <= 0 || rowHeight <= 0) { briefRowsPerColumn = Math.max(1, items.size()); briefColumns = 1; + extraColumns = 0; return; } briefRowsPerColumn = Math.max(1, availableHeight / rowHeight); briefColumns = (int) Math.ceil((double) items.size() / briefRowsPerColumn); + + // Calculate extra columns to allow snapping correctly at the end. + // We want the scrolling to stop precisely when the last column is fully visible + // on the right, but also ensure the left-most visible column is snapped to the left edge. + int colWidth = calculateCurrentColumnWidth(); + int viewportWidth = fileTable.getParent().getWidth(); + if (colWidth > 0 && viewportWidth > 0 && briefColumns > 0) { + int actualWidth = briefColumns * colWidth; + if (actualWidth > viewportWidth) { + // Smallest k such that (k * colWidth + viewportWidth) >= actualWidth + int k = (int) Math.ceil((double) (actualWidth - viewportWidth) / colWidth); + int neededWidth = k * colWidth + viewportWidth; + // How many extra columns of 'colWidth' would it take? + // We'll adjust the last one's width in updateColumnWidths + extraColumns = (int) Math.ceil((double) (neededWidth - actualWidth) / colWidth); + if (extraColumns == 0 && neededWidth > actualWidth) { + extraColumns = 1; // Need at least one to hold the fractional part + } + } else { + extraColumns = 0; + } + } else { + extraColumns = 0; + } } public FileItem getItemFromBriefLayout(int row, int column) { @@ -3233,6 +3298,8 @@ public class FilePanelTab extends JPanel { return getItem(row); } + if (column >= briefColumns) return null; + int index = column * briefRowsPerColumn + row; if (index >= 0 && index < items.size()) { return items.get(index); @@ -3251,7 +3318,7 @@ public class FilePanelTab extends JPanel { @Override public int getColumnCount() { if (viewMode == ViewMode.BRIEF) { - return briefColumns; + return briefColumns + extraColumns; } return 3; }