1939 lines
80 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|