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 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 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 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() ? "" : 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 getSelectedItems() { List 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 items = tableModel.items; if (items.isEmpty()) return; java.util.Comparator 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 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 criteria) { if (criteria == null || criteria.isEmpty()) return; if (tableModel == null || tableModel.items == null) return; java.util.Comparator 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 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 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 items = new ArrayList<>(); private String[] columnNames = {"Název", "Velikost", "Datum"}; public int briefColumns = 1; public int briefRowsPerColumn = 10; public void setItems(List 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; } } }