kf-manager/src/main/java/com/kfmanager/ui/FilePanelTab.java
2026-01-14 20:50:46 +01:00

1939 lines
80 KiB
Java

package com.kfmanager.ui;
import com.kfmanager.model.FileItem;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
/**
* Single tab in a panel - displays the contents of one directory
*/
public class FilePanelTab extends JPanel {
private File currentDirectory;
private JTable fileTable;
private FileTableModel tableModel;
private JLabel statusLabel;
private ViewMode viewMode = ViewMode.FULL;
private int briefCurrentColumn = 0;
private int briefColumnBeforeEnter = 0;
private Runnable onDirectoryChanged;
private Runnable onSwitchPanelRequested;
// Appearance customization
private Color selectionColor = new Color(184, 207, 229);
private Color selectionInactiveColor = new Color(220, 220, 220);
private Color markedColor = new Color(204, 153, 0);
// Sorting state for FULL mode header clicks
private int sortColumn = -1; // 0=name,1=size,2=date
private boolean sortAscending = true;
private com.kfmanager.config.AppConfig persistedConfig;
// If an archive was opened, we may extract it to a temp directory; track it so
// we can cleanup older temp directories when navigation changes.
private Path currentArchiveTempDir = null;
private File currentArchiveSourceFile = null;
public FilePanelTab(String initialPath) {
this.currentDirectory = new File(initialPath);
initComponents();
loadDirectory(currentDirectory);
}
/** Start inline rename for currently selected item (if single selection). */
public void startInlineRename() {
// Determine selected row/column according to view mode
int selRow = fileTable.getSelectedRow();
if (selRow < 0) return;
int editCol = 0;
if (viewMode == ViewMode.BRIEF) {
editCol = briefCurrentColumn;
// If briefCurrentColumn is out of range, pick column 0
if (editCol < 0 || editCol >= tableModel.getColumnCount()) editCol = 0;
} else {
editCol = 0; // name column in FULL
}
// Only allow editing if the cell represents a real item
if (!tableModel.isCellEditable(selRow, editCol)) return;
// Start editing and select all text in editor
boolean started = fileTable.editCellAt(selRow, editCol);
if (started) {
Component ed = fileTable.getEditorComponent();
if (ed instanceof JTextField) {
JTextField tf = (JTextField) ed;
tf.requestFocusInWindow();
tf.selectAll();
}
}
}
/**
* Ensure that column renderers are attached. Useful when the tab was just added to a container
* and the ColumnModel may not yet be in its final state.
*/
public void ensureRenderers() {
SwingUtilities.invokeLater(() -> {
updateColumnRenderers();
updateColumnWidths();
fileTable.revalidate();
fileTable.repaint();
});
}
// --- Appearance application methods ---
public void applyGlobalFont(Font font) {
if (font == null) return;
fileTable.setFont(font);
statusLabel.setFont(font);
// Update row height based on font metrics
FontMetrics fm = fileTable.getFontMetrics(font);
fileTable.setRowHeight(Math.max(18, fm.getHeight() + 4));
// If in BRIEF mode, recalculate brief layout (like on resize) so columns/rows fit
SwingUtilities.invokeLater(() -> {
if (viewMode == ViewMode.BRIEF) {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
updateColumnRenderers();
updateColumnWidths();
} else {
updateColumnWidths();
}
ensureRenderers();
fileTable.revalidate();
fileTable.repaint();
statusLabel.revalidate();
statusLabel.repaint();
});
}
public void applyBackgroundColor(Color bg) {
if (bg == null) return;
updateComponentBackground(this, bg);
fileTable.setBackground(bg);
statusLabel.setBackground(bg);
repaint();
}
private void updateComponentBackground(Container container, Color bg) {
if (container == null) return;
container.setBackground(bg);
boolean dark = isDark(bg);
for (Component c : container.getComponents()) {
if (c instanceof JPanel || c instanceof JToolBar || c instanceof JScrollPane || c instanceof JViewport || c instanceof JTabbedPane) {
c.setBackground(bg);
} else if (c instanceof JLabel || c instanceof JCheckBox || c instanceof JRadioButton) {
c.setForeground(dark ? Color.WHITE : Color.BLACK);
}
if (c instanceof Container) {
updateComponentBackground((Container) c, bg);
}
}
}
private boolean isDark(Color c) {
if (c == null) return false;
double darkness = 1 - (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255;
return darkness >= 0.5;
}
public void applySelectionColor(Color sel) {
if (sel == null) return;
this.selectionColor = sel;
// Derive an inactive variant
this.selectionInactiveColor = sel.brighter();
fileTable.repaint();
}
public void applyMarkedColor(Color mark) {
if (mark == null) return;
this.markedColor = mark;
fileTable.repaint();
}
/**
* Set callback to notify when directory changes
*/
public void setOnDirectoryChanged(Runnable callback) {
this.onDirectoryChanged = callback;
}
public void setOnSwitchPanelRequested(Runnable callback) {
this.onSwitchPanelRequested = callback;
}
private void initComponents() {
setLayout(new BorderLayout());
// Table showing files
tableModel = new FileTableModel();
// Use a custom JTable subclass to intercept mouse events so that mouse-driven
// selection is disabled while still allowing double-click to open items.
// Also override mouse-motion processing to suppress drag events and
// prevent any drag-and-drop transfer handling.
fileTable = new JTable(tableModel) {
@Override
protected void processMouseEvent(java.awt.event.MouseEvent e) {
// Show system-like context menu on popup trigger (right-click) without
// changing default mouse-driven selection behavior.
try {
if (e.isPopupTrigger()) {
int col = columnAtPoint(e.getPoint());
int row = rowAtPoint(e.getPoint());
FileItem item = null;
if (row >= 0) {
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(row, col);
if (item != null) {
int index = tableModel.items.indexOf(item);
if (index >= 0) {
int selRow = index % tableModel.briefRowsPerColumn;
int selCol = index / tableModel.briefRowsPerColumn;
briefCurrentColumn = selCol;
// Select the logical row so actions apply to this item
fileTable.setRowSelectionInterval(selRow, selRow);
}
}
} else {
item = tableModel.getItem(row);
if (item != null) {
fileTable.setRowSelectionInterval(row, row);
}
}
}
if (item != null) {
// Delegate to outer class to build and show the menu
FilePanelTab.this.showContextMenuForItem(item, e.getPoint());
}
e.consume();
return;
}
} catch (Exception ex) {
// best-effort: ignore exceptions here to avoid breaking mouse handling
}
// Intercept mouse events to prevent changing selection via mouse.
// Allow single-click to toggle marking of an item, allow BRIEF column
// tracking, and preserve double-click to open items. Dragging/presses
// that would normally change selection are consumed.
if (e.getID() == java.awt.event.MouseEvent.MOUSE_CLICKED) {
int col = columnAtPoint(e.getPoint());
int row = rowAtPoint(e.getPoint());
if (viewMode == ViewMode.BRIEF && col >= 0) {
briefCurrentColumn = col;
repaint();
}
// Single left-click should focus/select the item under cursor but
// should NOT toggle its marked state. This preserves keyboard
// marking (Insert) while making mouse clicks act as simple focus.
if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) {
if (row >= 0) {
// Convert brief layout coordinates to absolute index where needed
if (viewMode == ViewMode.BRIEF) {
FileItem item = tableModel.getItemFromBriefLayout(row, col);
if (item != null) {
int index = tableModel.items.indexOf(item);
if (index >= 0) {
int selRow = index % tableModel.briefRowsPerColumn;
int selCol = index / tableModel.briefRowsPerColumn;
briefCurrentColumn = selCol;
fileTable.setRowSelectionInterval(selRow, selRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true));
}
}
} else {
// FULL mode: rows map directly
fileTable.setRowSelectionInterval(row, row);
fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true));
}
fileTable.requestFocusInWindow();
repaint();
updateStatus();
}
}
// Double-click opens the item under cursor (directories)
if (e.getClickCount() == 2) {
openItemAtPoint(e.getPoint());
}
// Consume to avoid default selection behavior (no selection change by mouse)
e.consume();
return;
}
// Ignore mouse pressed/released/dragged events to prevent selection changes
if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED ||
e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED ||
e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) {
e.consume();
return;
}
super.processMouseEvent(e);
}
@Override
protected void processMouseMotionEvent(java.awt.event.MouseEvent e) {
// Block mouse-dragged events so dragging cannot change selection
// or initiate any drag behavior. Consume the event and do nothing.
if (e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) {
e.consume();
return;
}
super.processMouseMotionEvent(e);
}
};
// Disable Swing drag-and-drop support and transfer handling on the table
// to ensure no drag gestures or DnD occur.
fileTable.setDragEnabled(false);
fileTable.setTransferHandler(null);
try {
fileTable.setDropMode(null);
} catch (Exception ignore) {
// Some JVMs may not support null DropMode; ignore if unsupported.
}
// Prevent column reordering via header drag
if (fileTable.getTableHeader() != null) {
fileTable.getTableHeader().setReorderingAllowed(false);
// Set header renderer to show sort arrow when applicable
javax.swing.table.TableCellRenderer defaultHeaderRenderer = fileTable.getTableHeader().getDefaultRenderer();
fileTable.getTableHeader().setDefaultRenderer(new SortHeaderRenderer(defaultHeaderRenderer));
// Add header click listener to change sorting in FULL mode
fileTable.getTableHeader().addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (viewMode != ViewMode.FULL) return;
int col = fileTable.columnAtPoint(e.getPoint());
if (col < 0) return;
// Toggle sort order if same column, otherwise set ascending
if (sortColumn == col) {
sortAscending = !sortAscending;
} else {
sortColumn = col;
// Default sort direction: for names ascending, for size/date show newest/largest first
if (col == 0) {
sortAscending = true;
} else {
sortAscending = false;
}
}
sortItemsByColumn(sortColumn, sortAscending);
tableModel.fireTableDataChanged();
updateStatus();
if (persistedConfig != null) {
persistedConfig.setDefaultSortColumn(sortColumn);
persistedConfig.setDefaultSortAscending(sortAscending);
persistedConfig.saveConfig();
}
if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
}
});
}
fileTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
Font initialFont = new Font("Monospaced", Font.PLAIN, 12);
fileTable.setFont(initialFont);
// Set row height according to font metrics so rows scale with font size
FontMetrics fmInit = fileTable.getFontMetrics(initialFont);
fileTable.setRowHeight(Math.max(16, fmInit.getHeight() + 4));
// Default auto-resize: in FULL mode we allow automatic column resizing, in BRIEF
// mode we will disable auto-resize so horizontal scrolling works and full names
// remain visible.
fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
// Nastavit Cell Selection mode pouze v BRIEF režimu
fileTable.setCellSelectionEnabled(false);
fileTable.setRowSelectionAllowed(true);
fileTable.setColumnSelectionAllowed(false);
// Odstranit bordery z tabulky
fileTable.setShowGrid(false);
fileTable.setIntercellSpacing(new java.awt.Dimension(0, 0));
// Nastavit pozadí tabulky stejné jako pozadí panelu
fileTable.setBackground(this.getBackground());
fileTable.setOpaque(true);
JScrollPane scrollPane = new JScrollPane(fileTable);
// Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
add(scrollPane, BorderLayout.CENTER);
// Status bar
statusLabel = new JLabel(" ");
statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
add(statusLabel, BorderLayout.SOUTH);
// Add listener to handle panel resize
scrollPane.addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentResized(java.awt.event.ComponentEvent e) {
if (viewMode == ViewMode.BRIEF) {
// Při změně velikosti v BRIEF módu přepočítat layout
SwingUtilities.invokeLater(() -> {
// Zapamatovat si vybranou položku
String selectedItemName = null;
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
if (item != null) {
selectedItemName = item.getName();
}
}
// Přepočítat layout
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
updateColumnRenderers();
updateColumnWidths();
// Znovu vybrat položku
if (selectedItemName != null) {
selectItemByName(selectedItemName);
}
});
}
}
});
// Note: mouse handling is implemented inside the custom JTable above so
// that mouse-driven selection is disabled.
// Setup custom key handling (Home/End etc.)
setupKeyListeners();
// Ensure TAB key switches panels even when multiple tabs are open.
// Map TAB to an action that asks the top-level window to switch panels.
fileTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_TAB, 0), "switchPanelFromTab");
fileTable.getActionMap().put("switchPanelFromTab", new AbstractAction() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
if (onSwitchPanelRequested != null) {
onSwitchPanelRequested.run();
}
}
});
}
private void setupKeyListeners() {
fileTable.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(java.awt.event.KeyEvent e) {
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) {
openSelectedItem();
e.consume();
} else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_BACK_SPACE) {
navigateUp();
e.consume();
} else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_INSERT) {
toggleSelectionAndMoveDown();
e.consume();
} else if (viewMode == ViewMode.BRIEF) {
handleBriefKeyNavigation(e);
} else if (viewMode == ViewMode.FULL) {
// Support Home/End in FULL mode to jump to first/last row
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_HOME) {
if (tableModel.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0);
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
updateStatus();
}
e.consume();
} else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_END) {
int last = tableModel.getRowCount() - 1;
if (last >= 0) {
fileTable.setRowSelectionInterval(last, last);
fileTable.scrollRectToVisible(fileTable.getCellRect(last, 0, true));
updateStatus();
}
e.consume();
}
}
}
});
}
private void handleBriefKeyNavigation(java.awt.event.KeyEvent e) {
switch (e.getKeyCode()) {
case java.awt.event.KeyEvent.VK_UP:
case java.awt.event.KeyEvent.VK_DOWN:
handleBriefNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_DOWN);
e.consume();
break;
case java.awt.event.KeyEvent.VK_LEFT:
case java.awt.event.KeyEvent.VK_RIGHT:
handleBriefHorizontalNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_RIGHT);
e.consume();
break;
case java.awt.event.KeyEvent.VK_HOME:
if (tableModel.items.size() > 0) {
briefCurrentColumn = 0;
fileTable.setRowSelectionInterval(0, 0);
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
fileTable.repaint();
updateStatus();
}
e.consume();
break;
case java.awt.event.KeyEvent.VK_END:
if (tableModel.items.size() > 0) {
int lastIndex = tableModel.items.size() - 1;
int lastColumn = lastIndex / tableModel.briefRowsPerColumn;
int lastRow = lastIndex % tableModel.briefRowsPerColumn;
if (lastColumn < tableModel.getColumnCount()) {
briefCurrentColumn = lastColumn;
fileTable.setRowSelectionInterval(lastRow, lastRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, lastColumn, true));
fileTable.repaint();
updateStatus();
}
}
e.consume();
break;
case java.awt.event.KeyEvent.VK_PAGE_UP:
case java.awt.event.KeyEvent.VK_PAGE_DOWN:
handleBriefPageNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_PAGE_UP);
e.consume();
break;
}
}
private void handleBriefNavigation(boolean down) {
int currentRow = fileTable.getSelectedRow();
if (currentRow < 0) return;
int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow;
int newIndex = down ? currentIndex + 1 : currentIndex - 1;
if (newIndex < 0 || newIndex >= tableModel.items.size()) {
return;
}
int newColumn = newIndex / tableModel.briefRowsPerColumn;
int newRow = newIndex % tableModel.briefRowsPerColumn;
if (newColumn < tableModel.getColumnCount()) {
briefCurrentColumn = newColumn;
fileTable.setRowSelectionInterval(newRow, newRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true));
fileTable.repaint();
updateStatus();
}
}
private void handleBriefHorizontalNavigation(boolean right) {
int currentRow = fileTable.getSelectedRow();
if (currentRow < 0) return;
int newColumn = right ? briefCurrentColumn + 1 : briefCurrentColumn - 1;
int colCount = tableModel.getColumnCount();
// If moving beyond edges, jump to first/last item instead of doing nothing
if (newColumn < 0 || newColumn >= colCount) {
if (right && briefCurrentColumn >= colCount - 1) {
// Jump to last item
int lastIndex = Math.max(0, tableModel.items.size() - 1);
int lastCol = lastIndex / tableModel.briefRowsPerColumn;
int lastRow = lastIndex % tableModel.briefRowsPerColumn;
briefCurrentColumn = Math.min(lastCol, Math.max(0, colCount - 1));
fileTable.setRowSelectionInterval(lastRow, lastRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, briefCurrentColumn, true));
fileTable.repaint();
updateStatus();
} else if (!right && briefCurrentColumn <= 0) {
// Jump to first item
briefCurrentColumn = 0;
if (tableModel.items.size() > 0) {
fileTable.setRowSelectionInterval(0, 0);
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
}
fileTable.repaint();
updateStatus();
}
return;
}
FileItem item = tableModel.getItemFromBriefLayout(currentRow, newColumn);
int targetRow = currentRow;
if (item == null) {
for (int row = currentRow - 1; row >= 0; row--) {
item = tableModel.getItemFromBriefLayout(row, newColumn);
if (item != null) {
targetRow = row;
break;
}
}
if (item == null) return;
}
briefCurrentColumn = newColumn;
fileTable.setRowSelectionInterval(targetRow, targetRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(targetRow, briefCurrentColumn, true));
fileTable.repaint();
updateStatus();
}
private void handleBriefPageNavigation(boolean pageUp) {
int currentRow = fileTable.getSelectedRow();
if (currentRow < 0) return;
int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow;
Rectangle visible = fileTable.getVisibleRect();
int rowHeight = fileTable.getRowHeight();
int visibleRows = visible.height / rowHeight;
if (visibleRows <= 0) visibleRows = 1;
// Compute how many columns are visible in the current viewport (from left-most visible column)
int visibleColumns = 1;
try {
int colCount = fileTable.getColumnModel().getColumnCount();
// Find first visible column by using columnAtPoint on the left edge of the visible rect
int firstVisibleCol = fileTable.columnAtPoint(new Point(visible.x, visible.y));
if (firstVisibleCol < 0) firstVisibleCol = 0;
int acc = 0;
for (int c = firstVisibleCol; c < colCount; c++) {
int w = fileTable.getColumnModel().getColumn(c).getWidth();
// Determine the column's starting X by summing widths of prior columns
acc += w;
// If accumulated width (relative to firstVisibleCol) exceeds viewport width, stop
if (acc > visible.width) {
break;
}
visibleColumns = c - firstVisibleCol + 1;
}
} catch (Exception ex) {
visibleColumns = Math.max(1, fileTable.getColumnModel().getColumnCount());
}
if (visibleColumns <= 0) visibleColumns = 1;
int visibleItems = visibleRows * visibleColumns;
if (visibleItems <= 0) visibleItems = visibleRows;
int newIndex = pageUp ? currentIndex - visibleItems : currentIndex + visibleItems;
newIndex = Math.max(0, Math.min(newIndex, tableModel.items.size() - 1));
int newColumn = newIndex / tableModel.briefRowsPerColumn;
int newRow = newIndex % tableModel.briefRowsPerColumn;
// Clamp newColumn to available columns
int maxCols = tableModel.getColumnCount();
if (newColumn >= maxCols) {
newColumn = Math.max(0, maxCols - 1);
// Recompute newRow to fit within that column
int baseIndex = newColumn * tableModel.briefRowsPerColumn;
newRow = Math.min(tableModel.briefRowsPerColumn - 1, newIndex - baseIndex);
}
briefCurrentColumn = newColumn;
fileTable.setRowSelectionInterval(newRow, newRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true));
fileTable.repaint();
updateStatus();
}
public void loadDirectory(File directory) {
loadDirectory(directory, true);
}
private void loadDirectory(File directory, boolean autoSelectFirst) {
// If we are switching directories, cleanup any previously extracted archive temp dirs
cleanupArchiveTempDirIfNeeded(directory);
if (directory == null || !directory.isDirectory()) {
return;
}
this.currentDirectory = directory;
briefCurrentColumn = 0;
File[] files = directory.listFiles();
List<FileItem> items = new ArrayList<>();
File parent = directory.getParentFile();
if (parent != null) {
items.add(new FileItem(parent) {
@Override
public String getName() {
return "..";
}
});
}
if (files != null && files.length > 0) {
Arrays.sort(files, Comparator
.comparing((File f) -> !f.isDirectory())
.thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File file : files) {
items.add(new FileItem(file));
}
}
tableModel.setItems(items);
if (viewMode == ViewMode.BRIEF) {
boolean selectFirst = autoSelectFirst;
SwingUtilities.invokeLater(() -> {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
updateColumnRenderers();
updateColumnWidths();
fileTable.revalidate();
fileTable.repaint();
if (selectFirst && fileTable.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0);
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
}
});
} else {
if (autoSelectFirst && fileTable.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0);
}
}
updateStatus();
// Oznámit změnu adresáře
if (onDirectoryChanged != null) {
onDirectoryChanged.run();
}
}
/**
* Cleanup previous archive temp dir when navigating away from it.
*/
private void cleanupArchiveTempDirIfNeeded(File newDirectory) {
try {
if (currentArchiveTempDir != null) {
Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null;
if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) {
deleteTempDirRecursively(currentArchiveTempDir);
currentArchiveTempDir = null;
}
}
} catch (Exception ignore) {
// best-effort cleanup
}
}
private boolean isArchiveFile(File f) {
if (f == null) return false;
String n = f.getName().toLowerCase();
return n.endsWith(".zip") || n.endsWith(".jar");
}
private Path extractArchiveToTemp(File archive) {
if (archive == null || !archive.isFile()) return null;
try {
Path tempDir = Files.createTempDirectory("kfmanager-archive-");
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(archive.toPath()))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
// Normalize entry name and prevent zip-slip
Path resolved = tempDir.resolve(entryName).normalize();
if (!resolved.startsWith(tempDir)) {
// suspicious entry, skip
zis.closeEntry();
continue;
}
if (entry.isDirectory() || entryName.endsWith("/")) {
Files.createDirectories(resolved);
} else {
Path parent = resolved.getParent();
if (parent != null) Files.createDirectories(parent);
// Copy entry contents
Files.copy(zis, resolved, StandardCopyOption.REPLACE_EXISTING);
}
zis.closeEntry();
}
}
return tempDir;
} catch (IOException ex) {
// extraction failed; attempt best-effort cleanup
try {
if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir);
} catch (Exception ignore) {}
return null;
}
}
private void deleteTempDirRecursively(Path dir) {
if (dir == null) return;
try {
if (!Files.exists(dir)) return;
Files.walk(dir)
.sorted(java.util.Comparator.reverseOrder())
.forEach(p -> {
try { Files.deleteIfExists(p); } catch (Exception ignore) {}
});
} catch (IOException ignore) {
}
}
private void openSelectedItem() {
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
} else {
item = tableModel.getItem(selectedRow);
}
if (item == null) return;
if (item.getName().equals("..")) {
navigateUp();
} else if (isArchiveFile(item.getFile())) {
Path temp = extractArchiveToTemp(item.getFile());
if (temp != null) {
// Delete any previous temp dir if different
try {
if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) {
deleteTempDirRecursively(currentArchiveTempDir);
}
} catch (Exception ignore) {}
currentArchiveTempDir = temp;
currentArchiveSourceFile = item.getFile();
loadDirectory(temp.toFile());
}
} else if (item.isDirectory()) {
briefColumnBeforeEnter = briefCurrentColumn;
loadDirectory(item.getFile());
}
}
}
/**
* Open the item located at the given point (used for double-clicks while
* mouse-driven selection is blocked). This mirrors the behavior of
* openSelectedItem but works from a mouse location instead of the table
* selection.
*/
private void openItemAtPoint(Point p) {
int row = fileTable.rowAtPoint(p);
int col = fileTable.columnAtPoint(p);
if (row < 0) return;
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(row, col);
} else {
item = tableModel.getItem(row);
}
if (item == null) return;
if (item.getName().equals("..")) {
navigateUp();
} else if (isArchiveFile(item.getFile())) {
Path temp = extractArchiveToTemp(item.getFile());
if (temp != null) {
try {
if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) {
deleteTempDirRecursively(currentArchiveTempDir);
}
} catch (Exception ignore) {}
currentArchiveTempDir = temp;
currentArchiveSourceFile = item.getFile();
loadDirectory(temp.toFile());
}
} else if (item.isDirectory()) {
briefColumnBeforeEnter = briefCurrentColumn;
loadDirectory(item.getFile());
}
}
/**
* Show a context popup menu for a specific file item at the given point (table coordinates).
*/
private void showContextMenuForItem(FileItem item, Point tablePoint) {
if (item == null) return;
JPopupMenu menu = new JPopupMenu();
// Open
JMenuItem openItem = new JMenuItem("Open");
openItem.addActionListener(ae -> {
if (item.getName().equals("..")) return;
if (item.isDirectory()) {
loadDirectory(item.getFile());
} else {
try {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().open(item.getFile());
}
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot open: " + ex.getMessage()); } catch (Exception ignore) {}
}
}
});
menu.add(openItem);
// Edit
JMenuItem editItem = new JMenuItem("Edit");
editItem.addActionListener(ae -> {
if (item.isDirectory() || item.getName().equals("..")) return;
try {
String ext = persistedConfig != null ? persistedConfig.getExternalEditorPath() : null;
if (ext != null && !ext.trim().isEmpty()) {
java.util.List<String> cmd = new java.util.ArrayList<>();
cmd.add(ext);
cmd.add(item.getFile().getAbsolutePath());
new ProcessBuilder(cmd).start();
} else if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().edit(item.getFile());
}
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot edit: " + ex.getMessage()); } catch (Exception ignore) {}
}
});
menu.add(editItem);
// Show in folder / Reveal
JMenuItem revealItem = new JMenuItem("Show in folder");
revealItem.addActionListener(ae -> {
try {
String os = System.getProperty("os.name").toLowerCase();
File f = item.getFile();
if (os.contains("win")) {
new ProcessBuilder("explorer.exe", "/select,", f.getAbsolutePath()).start();
} else if (os.contains("mac")) {
new ProcessBuilder("open", "-R", f.getAbsolutePath()).start();
} else {
File parent = f.getParentFile();
if (parent != null) new ProcessBuilder("xdg-open", parent.getAbsolutePath()).start();
}
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot reveal file: " + ex.getMessage()); } catch (Exception ignore) {}
}
});
menu.add(revealItem);
// Copy path
JMenuItem copyPath = new JMenuItem("Copy path");
copyPath.addActionListener(ae -> {
try {
StringSelection sel = new StringSelection(item.getFile().getAbsolutePath());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, null);
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot copy path: " + ex.getMessage()); } catch (Exception ignore) {}
}
});
menu.add(copyPath);
// Delete
JMenuItem deleteItem = new JMenuItem("Delete");
deleteItem.addActionListener(ae -> {
int res = JOptionPane.showConfirmDialog(FilePanelTab.this,
"Really delete '" + item.getName() + "'?",
"Delete",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (res == JOptionPane.YES_OPTION) {
try {
java.util.List<FileItem> toDelete = new java.util.ArrayList<>();
toDelete.add(item);
com.kfmanager.service.FileOperations.delete(toDelete, null);
// reload current directory
loadDirectory(getCurrentDirectory());
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Delete failed: " + ex.getMessage()); } catch (Exception ignore) {}
}
}
});
menu.add(deleteItem);
// Properties
JMenuItem props = new JMenuItem("Properties");
props.addActionListener(ae -> {
try {
File f = item.getFile();
String info = String.format("Name: %s\nPath: %s\nSize: %s\nModified: %s\nReadable: %b, Writable: %b, Executable: %b",
f.getName(), f.getAbsolutePath(), item.isDirectory() ? "<DIR>" : formatSize(f.length()),
new java.util.Date(f.lastModified()).toString(), f.canRead(), f.canWrite(), f.canExecute());
JOptionPane.showMessageDialog(FilePanelTab.this, info, "Properties", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot show properties: " + ex.getMessage()); } catch (Exception ignore) {}
}
});
menu.add(props);
// Show the popup at the click location
SwingUtilities.invokeLater(() -> {
try {
menu.show(fileTable, tablePoint.x, tablePoint.y);
} catch (Exception ignore) {}
});
}
public void navigateUp() {
// If we're currently browsing an extracted archive root, navigate back to the
// original archive's parent and select the archive file.
try {
if (currentArchiveTempDir != null && currentArchiveSourceFile != null) {
Path cur = currentDirectory.toPath().toAbsolutePath().normalize();
if (cur.equals(currentArchiveTempDir)) {
File parent = currentArchiveSourceFile.getParentFile();
if (parent != null) {
String archiveName = currentArchiveSourceFile.getName();
// cleanup temp dir before switching back
deleteTempDirRecursively(currentArchiveTempDir);
currentArchiveTempDir = null;
currentArchiveSourceFile = null;
loadDirectory(parent, false);
if (viewMode == ViewMode.BRIEF) {
SwingUtilities.invokeLater(() -> {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
updateColumnRenderers();
updateColumnWidths();
selectItemByName(archiveName);
});
} else {
selectItemByName(archiveName);
}
}
return;
}
}
} catch (Exception ignore) {}
File parent = currentDirectory.getParentFile();
if (parent != null) {
String previousDirName = currentDirectory.getName();
loadDirectory(parent, false);
if (viewMode == ViewMode.BRIEF) {
SwingUtilities.invokeLater(() -> {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
updateColumnRenderers();
updateColumnWidths();
selectItemByName(previousDirName);
});
} else {
selectItemByName(previousDirName);
}
}
}
private void selectItemByName(String name) {
if (viewMode == ViewMode.BRIEF) {
for (int i = 0; i < tableModel.items.size(); i++) {
FileItem item = tableModel.items.get(i);
if (item.getName().equals(name)) {
int column = i / tableModel.briefRowsPerColumn;
int row = i % tableModel.briefRowsPerColumn;
if (column < tableModel.getColumnCount()) {
briefCurrentColumn = column;
fileTable.setRowSelectionInterval(row, row);
fileTable.scrollRectToVisible(fileTable.getCellRect(row, column, true));
fileTable.repaint();
updateStatus();
}
return;
}
}
} else {
for (int i = 0; i < tableModel.getRowCount(); i++) {
FileItem item = tableModel.getItem(i);
if (item != null && item.getName().equals(name)) {
fileTable.setRowSelectionInterval(i, i);
fileTable.scrollRectToVisible(fileTable.getCellRect(i, 0, true));
updateStatus();
return;
}
}
}
}
/**
* Public wrapper to select an item by name from outside this class.
* Useful for other UI components to request focusing a specific file.
*/
public void selectItem(String name) {
selectItemByName(name);
}
public void toggleSelectionAndMoveDown() {
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
} else {
item = tableModel.getItem(selectedRow);
}
if (item != null && !item.getName().equals("..")) {
item.setMarked(!item.isMarked());
fileTable.repaint();
if (viewMode == ViewMode.BRIEF) {
handleBriefNavigation(true);
} else {
int nextRow = selectedRow + 1;
if (nextRow < fileTable.getRowCount()) {
fileTable.setRowSelectionInterval(nextRow, nextRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(nextRow, 0, true));
}
}
updateStatus();
}
}
}
public List<FileItem> getSelectedItems() {
List<FileItem> selected = new ArrayList<>();
for (FileItem item : tableModel.items) {
if (item.isMarked() && !item.getName().equals("..")) {
selected.add(item);
}
}
if (selected.isEmpty()) {
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
} else {
item = tableModel.getItem(selectedRow);
}
if (item != null && !item.getName().equals("..")) {
selected.add(item);
}
}
}
return selected;
}
public void setViewMode(ViewMode mode) {
if (this.viewMode != mode) {
String selectedItemName = null;
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item = null;
if (this.viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
} else {
item = tableModel.getItem(selectedRow);
}
if (item != null) {
selectedItemName = item.getName();
}
}
this.viewMode = mode;
final String itemToSelect = selectedItemName;
SwingUtilities.invokeLater(() -> {
tableModel.updateViewMode(mode);
// Switch auto-resize behavior depending on mode so BRIEF can scroll horizontally
if (mode == ViewMode.BRIEF) {
fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
} else {
fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
}
// Hide table header in BRIEF mode to save vertical space and match requirements
if (fileTable.getTableHeader() != null) {
fileTable.getTableHeader().setVisible(mode != ViewMode.BRIEF);
}
briefCurrentColumn = 0;
updateColumnRenderers();
updateColumnWidths();
fileTable.revalidate();
fileTable.repaint();
if (itemToSelect != null) {
selectItemByName(itemToSelect);
} else if (fileTable.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0);
}
fileTable.requestFocusInWindow();
});
}
}
private void updateColumnRenderers() {
int columnCount = tableModel.getColumnCount();
if (columnCount == 0 || fileTable.getColumnModel().getColumnCount() != columnCount) {
// Sloupce tabulky se možná ještě nepřevedly po změně struktury.
// Zkusíme to znovu asynchronně po krátkém odložení.
SwingUtilities.invokeLater(() -> updateColumnRenderers());
return;
}
for (int i = 0; i < columnCount; i++) {
final int colIndex = i;
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(row, column);
} else {
item = tableModel.getItem(row);
}
if (item == null) {
setBackground(FilePanelTab.this.getBackground());
setText("");
setIcon(null);
return this;
}
// Prepare displayed text: wrap directory names in square brackets
String displayText = "";
if (viewMode == ViewMode.BRIEF) {
displayText = item.getName();
if (item.isDirectory()) {
displayText = "[" + displayText + "]";
}
} else {
// FULL mode: only the first column shows the name
if (column == 0) {
displayText = item.getName();
if (item.isDirectory()) {
displayText = "[" + displayText + "]";
}
} else if (column == 1) {
displayText = item.getFormattedSize();
} else if (column == 2) {
displayText = item.getFormattedDate();
} else {
displayText = item.getName();
}
}
setText(displayText);
boolean isMarked = item.isMarked();
boolean isCurrentCell = false;
if (viewMode == ViewMode.BRIEF) {
isCurrentCell = isSelected && column == briefCurrentColumn;
} else {
isCurrentCell = isSelected;
}
// Only show selection highlight when this table has keyboard focus.
// If the table is not focused, do not highlight any row (show normal background).
if (isCurrentCell && table.hasFocus()) {
setBackground(selectionColor);
} else {
setBackground(FilePanelTab.this.getBackground());
}
// Preserve the table's base font/style and only add the marked-bold
Font baseFont = table.getFont();
if (baseFont == null) baseFont = getFont();
int baseStyle = baseFont != null ? baseFont.getStyle() : Font.PLAIN;
if (isMarked) {
// Ensure marked items are at least bold, preserve italic if present
int newStyle = baseStyle | Font.BOLD;
setFont(baseFont.deriveFont(newStyle));
setForeground(markedColor);
} else {
// Preserve whatever style the base font has (do not force plain)
setFont(baseFont.deriveFont(baseStyle));
// Automatically adjust foreground contrast
if (isCurrentCell && table.hasFocus()) {
setForeground(isDark(selectionColor) ? Color.WHITE : Color.BLACK);
} else {
setForeground(isDark(FilePanelTab.this.getBackground()) ? Color.WHITE : Color.BLACK);
}
}
// Zobrazit ikonu pro názvy souborů
Icon icon = item.getIcon();
if (viewMode == ViewMode.BRIEF) {
// V BRIEF módu jsou všechny sloupce názvy
setIcon(icon);
} else if (viewMode == ViewMode.FULL && column == 0) {
// V FULL módu je ikona pouze v prvním sloupci (názvy)
setIcon(icon);
} else {
setIcon(null);
}
setBorder(null);
return this;
}
};
if (viewMode == ViewMode.FULL && colIndex == 1) {
renderer.setHorizontalAlignment(SwingConstants.RIGHT);
}
fileTable.getColumnModel().getColumn(i).setCellRenderer(renderer);
}
}
private void updateColumnWidths() {
if (viewMode == ViewMode.FULL) {
if (fileTable.getColumnModel().getColumnCount() == 3) {
fileTable.getColumnModel().getColumn(0).setPreferredWidth(300);
fileTable.getColumnModel().getColumn(1).setPreferredWidth(100);
fileTable.getColumnModel().getColumn(2).setPreferredWidth(150);
}
} else if (viewMode == ViewMode.BRIEF) {
int columnCount = tableModel.getColumnCount();
if (columnCount == 0) {
return;
}
// Pokud se ColumnModel ještě nepřizpůsobil nové struktuře (např. po fireTableStructureChanged()),
// počkej a zkus nastavit renderery později - jinak nové sloupce nedostanou renderer a ikony se
// neprojeví v FULL módu.
if (fileTable.getColumnModel().getColumnCount() != columnCount) {
SwingUtilities.invokeLater(() -> updateColumnRenderers());
return;
}
// 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 display = item.getName();
if (item.isDirectory()) {
display = "[" + display + "]";
}
int w = fm.stringWidth(display);
if (w > maxTextWidth) maxTextWidth = w;
Icon icon = item.getIcon();
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;
for (int i = 0; i < columnCount; i++) {
fileTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth);
}
}
}
private void updateStatus() {
long totalSize = 0;
int fileCount = 0;
int dirCount = 0;
int markedCount = 0;
if (viewMode == ViewMode.BRIEF) {
for (int i = 0; i < tableModel.items.size(); i++) {
FileItem item = tableModel.items.get(i);
if (item.isMarked() && !item.getName().equals("..")) {
markedCount++;
if (item.isDirectory()) {
dirCount++;
} else {
fileCount++;
totalSize += item.getSize();
}
}
}
} else {
for (int i = 0; i < tableModel.getRowCount(); i++) {
FileItem item = tableModel.getItem(i);
if (item != null && item.isMarked() && !item.getName().equals("..")) {
markedCount++;
if (item.isDirectory()) {
dirCount++;
} else {
fileCount++;
totalSize += item.getSize();
}
}
}
}
if (markedCount > 0) {
statusLabel.setText(String.format(" Označeno: %d souborů, %d adresářů (%s)",
fileCount, dirCount, formatSize(totalSize)));
} else {
int selectedRow = fileTable.getSelectedRow();
if (selectedRow >= 0) {
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
} else {
item = tableModel.getItem(selectedRow);
}
if (item != null && !item.getName().equals("..")) {
if (item.isDirectory()) {
// Always display directory names in square brackets
statusLabel.setText(String.format(" [%s] | %s",
item.getName(),
item.getFormattedDate()));
} else {
statusLabel.setText(String.format(" %s | %s | %s",
item.getName(),
formatSize(item.getSize()),
item.getFormattedDate()));
}
} else {
statusLabel.setText(String.format(" Položek: %d", tableModel.items.size()));
}
} else {
statusLabel.setText(String.format(" Položek: %d", tableModel.items.size()));
}
}
}
/**
* Sort items in the current table model according to column (FULL mode).
* column: 0=name, 1=size, 2=date
*/
private void sortItemsByColumn(int column, boolean asc) {
if (tableModel == null || tableModel.items == null) return;
java.util.List<FileItem> items = tableModel.items;
if (items.isEmpty()) return;
java.util.Comparator<FileItem> comp;
switch (column) {
case 1: // size
comp = (a, b) -> {
boolean da = a.isDirectory(), db = b.isDirectory();
if (da != db) return da ? -1 : 1;
if (da && db) return a.getName().compareToIgnoreCase(b.getName());
int r = Long.compare(a.getSize(), b.getSize());
if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
return r;
};
break;
case 2: // date
// Sort by modification time regardless of directory flag so "newest first" places the newest item
comp = (a, b) -> {
int r = Long.compare(a.getModified().getTime(), b.getModified().getTime());
if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
return r;
};
break;
default: // name
comp = (a, b) -> compareFileItemsByName(a, b);
break;
}
if (!asc) comp = comp.reversed();
items.sort(comp);
// Refresh table on EDT
SwingUtilities.invokeLater(() -> {
if (viewMode == ViewMode.BRIEF) {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
} else {
tableModel.fireTableDataChanged();
}
updateColumnRenderers();
updateColumnWidths();
if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
});
}
/**
* Compare two FileItem objects by name with the following enhancements:
* - directories come before files
* - visible files come before hidden files
* - natural numeric-aware comparison ("file2" < "file10")
* - when names are equal ignoring case, prefer uppercase letters earlier (so "Apple" before "apple")
*/
private int compareFileItemsByName(FileItem a, FileItem b) {
if (a == b) return 0;
// Directories first
boolean da = a.isDirectory(), db = b.isDirectory();
if (da != db) return da ? -1 : 1;
// Hidden files ordering based on config (default: hidden last)
boolean hiddenLast = true;
try {
if (persistedConfig != null) hiddenLast = persistedConfig.getHiddenFilesLast();
} catch (Exception ignore) {}
try {
boolean ha = a.getFile() != null && a.getFile().isHidden();
boolean hb = b.getFile() != null && b.getFile().isHidden();
if (ha != hb) return hiddenLast ? (ha ? 1 : -1) : (ha ? -1 : 1);
} catch (Exception ignore) {}
String s1 = a.getName() != null ? a.getName() : "";
String s2 = b.getName() != null ? b.getName() : "";
// Optionally ignore leading dots (treat ".name" as "name") based on config
try {
if (persistedConfig != null && persistedConfig.getIgnoreLeadingDot()) {
s1 = s1.replaceFirst("^\\.+", "");
s2 = s2.replaceFirst("^\\.+", "");
}
} catch (Exception ignore) {}
// Numeric-aware vs simple compare based on config
boolean numericEnabled = true;
try { if (persistedConfig != null) numericEnabled = persistedConfig.getNumericSortEnabled(); } catch (Exception ignore) {}
if (numericEnabled) {
// Use natural compare; uppercase preference handled inside
return naturalCompareWithUppercasePreference(s1, s2);
} else {
// simple case-insensitive compare, optionally uppercase preference
int ci = s1.compareToIgnoreCase(s2);
if (ci != 0) return ci;
boolean uppercasePref = true;
try { if (persistedConfig != null) uppercasePref = persistedConfig.getUppercasePriority(); } catch (Exception ignore) {}
if (uppercasePref) {
// prefer uppercase earlier
int len = Math.min(s1.length(), s2.length());
for (int k = 0; k < len; k++) {
char aChar = s1.charAt(k);
char bChar = s2.charAt(k);
if (aChar == bChar) continue;
char la = Character.toLowerCase(aChar);
char lb = Character.toLowerCase(bChar);
if (la != lb) return la - lb;
boolean ua = Character.isUpperCase(aChar);
boolean ub = Character.isUpperCase(bChar);
if (ua != ub) return ua ? -1 : 1;
}
return s1.length() - s2.length();
} else {
return s1.compareTo(s2);
}
}
}
/**
* Natural compare of two strings with numeric awareness and uppercase preference.
*/
private int naturalCompareWithUppercasePreference(String s1, String s2) {
int i = 0, j = 0;
int n1 = s1.length(), n2 = s2.length();
while (i < n1 && j < n2) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(j);
// If both are digits, compare whole number sequences numerically
if (Character.isDigit(c1) && Character.isDigit(c2)) {
int start1 = i, start2 = j;
while (i < n1 && Character.isDigit(s1.charAt(i))) i++;
while (j < n2 && Character.isDigit(s2.charAt(j))) j++;
String num1 = s1.substring(start1, i);
String num2 = s2.substring(start2, j);
// Remove leading zeros for numeric comparison, but keep length for tie-break
String nz1 = num1.replaceFirst("^0+(?!$)", "");
String nz2 = num2.replaceFirst("^0+(?!$)", "");
if (nz1.length() != nz2.length()) return nz1.length() - nz2.length();
int cmp = nz1.compareTo(nz2);
if (cmp != 0) return cmp;
// If equal numerically, shorter original with fewer leading zeros first
if (num1.length() != num2.length()) return num1.length() - num2.length();
continue;
}
// Non-digit comparison: case-insensitive first
StringBuilder sb1 = new StringBuilder();
StringBuilder sb2 = new StringBuilder();
int ti = i, tj = j;
while (ti < n1 && !Character.isDigit(s1.charAt(ti))) {
sb1.append(s1.charAt(ti));
ti++;
}
while (tj < n2 && !Character.isDigit(s2.charAt(tj))) {
sb2.append(s2.charAt(tj));
tj++;
}
String part1 = sb1.toString();
String part2 = sb2.toString();
int ci = compareStringPartWithUppercasePreference(part1, part2);
if (ci != 0) return ci;
i = ti;
j = tj;
}
// If one string ended earlier, shorter one is first
if (i < n1) return 1; // s2 ended, s1 longer -> s2 first
if (j < n2) return -1;
return 0;
}
/**
* Compare two non-digit string parts: case-insensitive, but if equal ignoring case,
* prefer the one with uppercase letters earlier.
*/
private int compareStringPartWithUppercasePreference(String p1, String p2) {
int ci = p1.compareToIgnoreCase(p2);
if (ci != 0) return ci;
// If equal ignoring case, prefer uppercase earlier
int len = Math.min(p1.length(), p2.length());
for (int k = 0; k < len; k++) {
char a = p1.charAt(k);
char b = p2.charAt(k);
if (a == b) continue;
char la = Character.toLowerCase(a);
char lb = Character.toLowerCase(b);
if (la != lb) return la - lb; // different letters
// same letter ignoring case, decide by uppercase preference
boolean ua = Character.isUpperCase(a);
boolean ub = Character.isUpperCase(b);
if (ua != ub) return ua ? -1 : 1; // uppercase comes first
}
// If equal up to min length, shorter one first
return p1.length() - p2.length();
}
/** Header renderer that decorates the column title with an arrow for the sort column/direction */
private class SortHeaderRenderer implements javax.swing.table.TableCellRenderer {
private final javax.swing.table.TableCellRenderer delegate;
public SortHeaderRenderer(javax.swing.table.TableCellRenderer delegate) {
this.delegate = delegate;
}
@Override
public java.awt.Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus,
int row, int column) {
java.awt.Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (c instanceof javax.swing.JLabel) {
javax.swing.JLabel lbl = (javax.swing.JLabel) c;
String txt = value != null ? value.toString() : "";
if (sortColumn == column && fileTable.getTableHeader().isVisible()) {
String arrow = sortAscending ? "" : "";
lbl.setText(txt + arrow);
} else {
lbl.setText(txt);
}
}
return c;
}
}
/**
* Provide AppConfig so this tab can persist and restore sort settings.
*/
public void setAppConfig(com.kfmanager.config.AppConfig cfg) {
this.persistedConfig = cfg;
// Apply persisted sort if present
if (cfg != null) {
java.util.List<String> multi = cfg.getMultipleSortCriteria();
if (multi != null && !multi.isEmpty()) {
applyMultipleSortCriteria(multi);
} else {
int col = cfg.getDefaultSortColumn();
boolean asc = cfg.getDefaultSortAscending();
if (col >= 0) {
this.sortColumn = col;
this.sortAscending = asc;
sortItemsByColumn(sortColumn, sortAscending);
SwingUtilities.invokeLater(() -> {
tableModel.fireTableDataChanged();
if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
});
}
}
}
}
/**
* Apply a list of composite sort criteria in order (e.g. "name:asc", "size:desc").
*/
private void applyMultipleSortCriteria(java.util.List<String> criteria) {
if (criteria == null || criteria.isEmpty()) return;
if (tableModel == null || tableModel.items == null) return;
java.util.Comparator<FileItem> comp = null;
for (String c : criteria) {
if (c == null || c.trim().isEmpty()) continue;
String[] parts = c.split(":");
String field = parts[0].trim().toLowerCase();
boolean asc = true;
if (parts.length > 1 && parts[1].trim().equalsIgnoreCase("desc")) asc = false;
java.util.Comparator<FileItem> partComp;
switch (field) {
case "size":
partComp = (a, b) -> {
boolean da = a.isDirectory(), db = b.isDirectory();
if (da != db) return da ? -1 : 1;
if (da && db) return a.getName().compareToIgnoreCase(b.getName());
int r = Long.compare(a.getSize(), b.getSize());
if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
return r;
};
break;
case "date":
partComp = (a, b) -> {
int r = Long.compare(a.getModified().getTime(), b.getModified().getTime());
if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
return r;
};
break;
default:
partComp = (a, b) -> {
boolean da = a.isDirectory(), db = b.isDirectory();
if (da != db) return da ? -1 : 1;
return a.getName().compareToIgnoreCase(b.getName());
};
break;
}
if (!asc) partComp = partComp.reversed();
if (comp == null) comp = partComp;
else comp = comp.thenComparing(partComp);
}
if (comp == null) return;
java.util.List<FileItem> items = tableModel.items;
items.sort(comp);
SwingUtilities.invokeLater(() -> {
if (viewMode == ViewMode.BRIEF) {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
} else {
tableModel.fireTableDataChanged();
}
updateColumnRenderers();
updateColumnWidths();
if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
});
}
private String formatSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
// Gettery
public JTable getFileTable() {
return fileTable;
}
public File getCurrentDirectory() {
return currentDirectory;
}
public ViewMode getViewMode() {
return viewMode;
}
// FileTableModel - stejný jako v původním FilePanel
private class FileTableModel extends AbstractTableModel {
private List<FileItem> items = new ArrayList<>();
private String[] columnNames = {"Název", "Velikost", "Datum"};
public int briefColumns = 1;
public int briefRowsPerColumn = 10;
public void setItems(List<FileItem> items) {
this.items = items;
if (viewMode == ViewMode.BRIEF) {
calculateBriefLayout();
}
fireTableDataChanged();
}
public void updateViewMode(ViewMode mode) {
if (mode == ViewMode.BRIEF) {
calculateBriefLayout();
}
fireTableStructureChanged();
}
public void calculateBriefLayout() {
if (items.isEmpty()) {
briefColumns = 1;
briefRowsPerColumn = 1;
return;
}
int availableHeight = fileTable.getParent().getHeight();
int rowHeight = fileTable.getRowHeight();
if (availableHeight <= 0 || rowHeight <= 0) {
briefRowsPerColumn = Math.max(1, items.size());
briefColumns = 1;
return;
}
briefRowsPerColumn = Math.max(1, availableHeight / rowHeight);
briefColumns = (int) Math.ceil((double) items.size() / briefRowsPerColumn);
}
public FileItem getItemFromBriefLayout(int row, int column) {
if (viewMode != ViewMode.BRIEF) {
return getItem(row);
}
int index = column * briefRowsPerColumn + row;
if (index >= 0 && index < items.size()) {
return items.get(index);
}
return null;
}
@Override
public int getRowCount() {
if (viewMode == ViewMode.BRIEF) {
return briefRowsPerColumn;
}
return items.size();
}
@Override
public int getColumnCount() {
if (viewMode == ViewMode.BRIEF) {
return briefColumns;
}
return 3;
}
@Override
public String getColumnName(int column) {
if (viewMode == ViewMode.BRIEF) {
return "";
}
return columnNames[column];
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
if (viewMode == ViewMode.BRIEF) {
FileItem item = getItemFromBriefLayout(rowIndex, columnIndex);
return item != null ? item.getName() : "";
}
FileItem item = getItem(rowIndex);
if (item == null) {
return "";
}
switch (columnIndex) {
case 0: return item.getName();
case 1: return item.getFormattedSize();
case 2: return item.getFormattedDate();
default: return "";
}
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if (viewMode == ViewMode.BRIEF) {
FileItem it = getItemFromBriefLayout(rowIndex, columnIndex);
return it != null; // allow editing name in brief cells
}
// In FULL mode only the name column is editable
return columnIndex == 0;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
String newName = aValue != null ? aValue.toString().trim() : null;
if (newName == null || newName.isEmpty()) return;
FileItem item = null;
if (viewMode == ViewMode.BRIEF) {
item = getItemFromBriefLayout(rowIndex, columnIndex);
} else {
item = getItem(rowIndex);
}
if (item == null) return;
// Perform rename using FileOperations and refresh the directory
try {
com.kfmanager.service.FileOperations.rename(item.getFile(), newName);
// reload current directory to reflect updated names
FilePanelTab.this.loadDirectory(FilePanelTab.this.getCurrentDirectory());
// After reload, select the renamed item and focus the table
SwingUtilities.invokeLater(() -> {
try {
FilePanelTab.this.selectItem(newName);
FilePanelTab.this.getFileTable().requestFocusInWindow();
} catch (Exception ignore) {}
});
} catch (Exception ex) {
// show error to user
try {
JOptionPane.showMessageDialog(FilePanelTab.this,
"Rename failed: " + ex.getMessage(),
"Error",
JOptionPane.ERROR_MESSAGE);
} catch (Exception ignore) {}
}
}
public FileItem getItem(int index) {
if (index >= 0 && index < items.size()) {
return items.get(index);
}
return null;
}
}
}