kf-manager/src/main/java/com/kfmanager/ui/MainWindow.java
2026-01-15 12:31:20 +01:00

1462 lines
55 KiB
Java

package com.kfmanager.ui;
import com.kfmanager.config.AppConfig;
import com.kfmanager.model.FileItem;
import com.kfmanager.service.FileOperations;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.List;
/**
* Main application window with two panels
*/
public class MainWindow extends JFrame {
private FilePanel leftPanel;
private FilePanel rightPanel;
private FilePanel activePanel;
private JPanel buttonPanel;
private JTextField commandLine;
private AppConfig config;
private static final String APP_VERSION = "0.0.1";
public MainWindow() {
super("KF Manager v" + APP_VERSION);
// Load configuration
config = new AppConfig();
initComponents();
setupKeyBindings();
// Apply appearance from saved configuration at startup
applyAppearanceSettings();
// Restore window size and position from configuration
config.restoreWindowState(this);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
// Add listener to save configuration on exit
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
MainWindow.this.saveConfigAndExit();
}
});
// After start, set focus and selection to the left panel
SwingUtilities.invokeLater(() -> {
leftPanel.getFileTable().requestFocus();
});
}
private void initComponents() {
setLayout(new BorderLayout());
// Toolbar
createToolBar();
// Panel containing the two file panels
JPanel mainPanel = new JPanel(new GridLayout(1, 2, 5, 0));
// Left panel - load path and ViewMode from configuration
String leftPath = config.getLeftPanelPath();
leftPanel = new FilePanel(leftPath);
leftPanel.setAppConfig(config);
leftPanel.setBorder(BorderFactory.createTitledBorder("Left panel"));
// Provide a callback so tabs inside the panel can request switching panels with TAB
leftPanel.setSwitchPanelCallback(() -> switchPanelsFromChild());
// Load and set ViewMode for left panel
try {
ViewMode leftViewMode = ViewMode.valueOf(config.getLeftPanelViewMode());
leftPanel.setViewMode(leftViewMode);
} catch (IllegalArgumentException e) {
// Výchozí hodnota FULL je již nastavena
}
// Right panel - load path and ViewMode from configuration
String rightPath = config.getRightPanelPath();
rightPanel = new FilePanel(rightPath);
rightPanel.setAppConfig(config);
rightPanel.setBorder(BorderFactory.createTitledBorder("Right panel"));
// Provide a callback so tabs inside the panel can request switching panels with TAB
rightPanel.setSwitchPanelCallback(() -> switchPanelsFromChild());
// Load and set ViewMode for right panel
try {
ViewMode rightViewMode = ViewMode.valueOf(config.getRightPanelViewMode());
rightPanel.setViewMode(rightViewMode);
} catch (IllegalArgumentException e) {
// Výchozí hodnota FULL je již nastavena
}
mainPanel.add(leftPanel);
mainPanel.add(rightPanel);
add(mainPanel, BorderLayout.CENTER);
// Set left panel as active by default
activePanel = leftPanel;
updateActivePanelBorder();
// Restore saved tabs for both panels if present in configuration
try {
int leftCount = config.getLeftPanelTabCount();
if (leftCount > 0) {
java.util.List<String> paths = new java.util.ArrayList<>();
java.util.List<String> modes = new java.util.ArrayList<>();
for (int i = 0; i < leftCount; i++) {
String p = config.getLeftPanelTabPath(i);
if (p == null) p = System.getProperty("user.home");
paths.add(p);
modes.add(config.getLeftPanelTabViewMode(i));
}
int sel = config.getLeftPanelSelectedIndex();
leftPanel.restoreTabs(paths, modes, sel);
}
} catch (Exception ex) {
// ignore and keep default
}
try {
int rightCount = config.getRightPanelTabCount();
if (rightCount > 0) {
java.util.List<String> paths = new java.util.ArrayList<>();
java.util.List<String> modes = new java.util.ArrayList<>();
for (int i = 0; i < rightCount; i++) {
String p = config.getRightPanelTabPath(i);
if (p == null) p = System.getProperty("user.home");
paths.add(p);
modes.add(config.getRightPanelTabViewMode(i));
}
int sel = config.getRightPanelSelectedIndex();
rightPanel.restoreTabs(paths, modes, sel);
}
} catch (Exception ex) {
// ignore and keep default
}
// Focus listeners to track active panel
leftPanel.getFileTable().addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
activePanel = leftPanel;
updateActivePanelBorder();
// Zajistit, že je vybrán nějaký řádek
JTable leftTable = leftPanel.getFileTable();
if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) {
leftTable.setRowSelectionInterval(0, 0);
}
// Překreslit oba panely
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
}
@Override
public void focusLost(FocusEvent e) {
// Překreslit při ztrátě focusu
leftPanel.getFileTable().repaint();
}
});
rightPanel.getFileTable().addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
activePanel = rightPanel;
updateActivePanelBorder();
// Zajistit, že je vybrán nějaký řádek
JTable rightTable = rightPanel.getFileTable();
if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) {
rightTable.setRowSelectionInterval(0, 0);
}
// Překreslit oba panely
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
}
@Override
public void focusLost(FocusEvent e) {
// Překreslit při ztrátě focusu
rightPanel.getFileTable().repaint();
}
});
// Click on panel anywhere should request focus to its table
leftPanel.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
leftPanel.getFileTable().requestFocusInWindow();
}
});
rightPanel.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
rightPanel.getFileTable().requestFocusInWindow();
}
});
// Add TAB handler to switch between panels
addTabKeyHandler(leftPanel.getFileTable());
addTabKeyHandler(rightPanel.getFileTable());
// Add command line focus redirection
addCommandLineRedirect(leftPanel.getFileTable());
addCommandLineRedirect(rightPanel.getFileTable());
// Container for everything below the file panels
JPanel bottomContainer = new JPanel(new BorderLayout());
// Command line panel
JPanel cmdPanel = new JPanel(new BorderLayout(5, 0));
cmdPanel.setBorder(BorderFactory.createEmptyBorder(2, 5, 0, 5));
JLabel cmdLabel = new JLabel(System.getProperty("user.name") + ">");
cmdLabel.setFont(new Font("Monospaced", Font.BOLD, 12));
cmdPanel.add(cmdLabel, BorderLayout.WEST);
commandLine = new JTextField();
commandLine.setFont(new Font("Monospaced", Font.PLAIN, 12));
commandLine.setFocusTraversalKeysEnabled(false);
commandLine.addActionListener(e -> executeCommand(commandLine.getText()));
// Let the panels catch focus back if user presses ESC or TAB in command line
commandLine.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
commandLine.setText("");
activePanel.getFileTable().requestFocusInWindow();
e.consume();
} else if (e.getKeyCode() == KeyEvent.VK_TAB) {
activePanel.getFileTable().requestFocusInWindow();
e.consume();
}
}
});
cmdPanel.add(commandLine, BorderLayout.CENTER);
bottomContainer.add(cmdPanel, BorderLayout.NORTH);
// Bottom panel with buttons
createButtonPanel();
bottomContainer.add(buttonPanel, BorderLayout.SOUTH);
add(bottomContainer, BorderLayout.SOUTH);
// Menu
createMenuBar();
}
/**
* Create toolbar with buttons for changing view mode
*/
private void createToolBar() {
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
toolBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
// Button for BRIEF mode
JButton btnBrief = new JButton("☰ Brief");
btnBrief.setToolTipText("Brief mode - multiple columns (Ctrl+F1)");
btnBrief.setFocusable(false);
btnBrief.addActionListener(e -> {
if (activePanel != null) {
activePanel.setViewMode(ViewMode.BRIEF);
}
});
// Button for FULL mode
JButton btnFull = new JButton("▤ Full");
btnFull.setToolTipText("Full mode - full information (Ctrl+F2)");
btnFull.setFocusable(false);
btnFull.addActionListener(e -> {
if (activePanel != null) {
activePanel.setViewMode(ViewMode.FULL);
}
});
toolBar.add(btnBrief);
toolBar.add(btnFull);
toolBar.addSeparator();
// Informační label
JLabel infoLabel = new JLabel(" View Mode ");
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.PLAIN, 10f));
toolBar.add(infoLabel);
add(toolBar, BorderLayout.NORTH);
}
/**
* Create button panel (like Total Commander)
*/
private void createButtonPanel() {
buttonPanel = new JPanel(new GridLayout(1, 9, 5, 5));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
JButton btnView = new JButton("F3 View");
btnView.addActionListener(e -> viewFile());
JButton btnEdit = new JButton("F4 Edit");
btnEdit.addActionListener(e -> editFile());
JButton btnCopy = new JButton("F5 Copy");
btnCopy.addActionListener(e -> copyFiles());
JButton btnMove = new JButton("F6 Move");
btnMove.addActionListener(e -> moveFiles());
JButton btnNewDir = new JButton("F7 New Dir");
btnNewDir.addActionListener(e -> createNewDirectory());
JButton btnDelete = new JButton("F8 Delete");
btnDelete.addActionListener(e -> deleteFiles());
JButton btnTerminal = new JButton("F9 Terminal");
btnTerminal.addActionListener(e -> openTerminal());
JButton btnRename = new JButton("Shift+F6 Rename");
btnRename.addActionListener(e -> renameFile());
JButton btnExit = new JButton("F10 Exit");
btnExit.addActionListener(e -> saveConfigAndExit());
buttonPanel.add(btnView);
buttonPanel.add(btnEdit);
buttonPanel.add(btnCopy);
buttonPanel.add(btnMove);
buttonPanel.add(btnNewDir);
buttonPanel.add(btnDelete);
buttonPanel.add(btnTerminal);
buttonPanel.add(btnRename);
buttonPanel.add(btnExit);
}
/**
* Create menu bar
*/
private void createMenuBar() {
JMenuBar menuBar = new JMenuBar();
// File menu
JMenu fileMenu = new JMenu("File");
JMenuItem searchItem = new JMenuItem("Search...");
searchItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK));
searchItem.addActionListener(e -> showSearchDialog());
JMenuItem refreshItem = new JMenuItem("Refresh");
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
refreshItem.addActionListener(e -> refreshPanels());
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0));
exitItem.addActionListener(e -> saveConfigAndExit());
fileMenu.add(searchItem);
fileMenu.add(refreshItem);
fileMenu.addSeparator();
fileMenu.add(exitItem);
// View menu
JMenu viewMenu = new JMenu("View");
JMenuItem fullViewItem = new JMenuItem("Full details");
fullViewItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_DOWN_MASK));
fullViewItem.addActionListener(e -> setActiveViewMode(ViewMode.FULL));
JMenuItem briefViewItem = new JMenuItem("Names only");
briefViewItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.CTRL_DOWN_MASK));
briefViewItem.addActionListener(e -> setActiveViewMode(ViewMode.BRIEF));
viewMenu.add(fullViewItem);
viewMenu.add(briefViewItem);
// Settings menu
JMenu settingsMenu = new JMenu("Settings");
JMenuItem appearanceItem = new JMenuItem("Appearance...");
appearanceItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.ALT_DOWN_MASK));
appearanceItem.addActionListener(e -> showSettingsDialog());
settingsMenu.add(appearanceItem);
// Help menu
JMenu helpMenu = new JMenu("Help");
JMenuItem aboutItem = new JMenuItem("About");
aboutItem.addActionListener(e -> showAboutDialog());
helpMenu.add(aboutItem);
menuBar.add(fileMenu);
menuBar.add(viewMenu);
menuBar.add(settingsMenu);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
/**
* Show settings dialog and apply appearance changes
*/
private void showSettingsDialog() {
SettingsDialog dlg = new SettingsDialog(this, config, () -> applyAppearanceSettings());
dlg.setVisible(true);
// After dialog closed, ensure appearance applied
applyAppearanceSettings();
}
/**
* Apply appearance settings (font/colors) from config to UI components.
*/
private void applyAppearanceSettings() {
Font gfont = config.getGlobalFont();
if (gfont != null) {
// Apply to toolbars, buttons and tables
SwingUtilities.invokeLater(() -> {
for (Component c : getContentPane().getComponents()) {
c.setFont(gfont);
}
// Apply to panels' tables
if (leftPanel != null && leftPanel.getFileTable() != null) {
leftPanel.applyGlobalFont(gfont);
}
if (rightPanel != null && rightPanel.getFileTable() != null) {
rightPanel.applyGlobalFont(gfont);
}
});
}
Color bg = config.getBackgroundColor();
if (bg != null) {
SwingUtilities.invokeLater(() -> {
updateComponentBackground(getContentPane(), bg);
if (leftPanel != null) leftPanel.applyBackgroundColor(bg);
if (rightPanel != null) rightPanel.applyBackgroundColor(bg);
});
}
Color sel = config.getSelectionColor();
if (sel != null) {
if (leftPanel != null) leftPanel.applySelectionColor(sel);
if (rightPanel != null) rightPanel.applySelectionColor(sel);
}
Color mark = config.getMarkedColor();
if (mark != null) {
if (leftPanel != null) leftPanel.applyMarkedColor(mark);
if (rightPanel != null) rightPanel.applyMarkedColor(mark);
}
// Re-propagate AppConfig to panels so they can pick up sorting and other config changes
SwingUtilities.invokeLater(() -> {
try {
if (leftPanel != null) leftPanel.setAppConfig(config);
} catch (Exception ignore) {}
try {
if (rightPanel != null) rightPanel.setAppConfig(config);
} catch (Exception ignore) {}
});
}
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 instanceof JButton) {
c.setBackground(bg);
}
if (c instanceof JLabel || c instanceof JCheckBox || c instanceof JRadioButton || c instanceof JButton) {
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;
}
/**
* Setup keyboard shortcuts
*/
private void setupKeyBindings() {
JRootPane rootPane = getRootPane();
// F3 - Prohlížeč
rootPane.registerKeyboardAction(e -> viewFile(),
KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F4 - Editor
rootPane.registerKeyboardAction(e -> editFile(),
KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F5 - Copy
rootPane.registerKeyboardAction(e -> copyFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+F5 - Zip
rootPane.registerKeyboardAction(e -> zipFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+F9 - Unzip
rootPane.registerKeyboardAction(e -> unzipFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_F9, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+O - Settings
rootPane.registerKeyboardAction(e -> showSettingsDialog(),
KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F6 - Move
rootPane.registerKeyboardAction(e -> moveFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Shift+F6 - Rename (alternative to F9)
rootPane.registerKeyboardAction(e -> renameFile(),
KeyStroke.getKeyStroke(KeyEvent.VK_F6, InputEvent.SHIFT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F7 - New directory
rootPane.registerKeyboardAction(e -> createNewDirectory(),
KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F8 - Delete
rootPane.registerKeyboardAction(e -> deleteFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// F9 - Open terminal
rootPane.registerKeyboardAction(e -> openTerminal(),
KeyStroke.getKeyStroke(KeyEvent.VK_F9, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+TAB - switch tabs in active panel
rootPane.registerKeyboardAction(e -> {
if (activePanel != null) {
activePanel.nextTab();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// ESC - global escape to return focus to panels
rootPane.registerKeyboardAction(e -> {
if (activePanel != null) {
// Always clear command line and return focus to panels
commandLine.setText("");
activePanel.getFileTable().requestFocusInWindow();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Delete key - global delete binding (also added per-table)
rootPane.registerKeyboardAction(e -> deleteFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Shift+Delete - treat as delete as well
rootPane.registerKeyboardAction(e -> deleteFiles(),
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// No direct F9 keyboard binding: inline rename should only be triggered by Shift+F6
// TAB - switch panel (global) - works even when additional tabs are opened
rootPane.registerKeyboardAction(e -> switchPanels(),
KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+F1 - Select drive for left panel
rootPane.registerKeyboardAction(e -> selectDriveForLeftPanel(),
KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+F2 - Select drive for right panel
rootPane.registerKeyboardAction(e -> selectDriveForRightPanel(),
KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Alt+F7 - Search (changed from Ctrl+F)
rootPane.registerKeyboardAction(e -> showSearchDialog(),
KeyStroke.getKeyStroke(KeyEvent.VK_F7, InputEvent.ALT_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+F1 - Full details
rootPane.registerKeyboardAction(e -> setActiveViewMode(ViewMode.FULL),
KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+F2 - Names only
rootPane.registerKeyboardAction(e -> setActiveViewMode(ViewMode.BRIEF),
KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+T - New tab
rootPane.registerKeyboardAction(e -> addNewTabToActivePanel(),
KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+W - Close current tab
rootPane.registerKeyboardAction(e -> closeCurrentTabInActivePanel(),
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
}
/**
* Update panel borders according to active panel
*/
private void updateActivePanelBorder() {
leftPanel.setBorder(BorderFactory.createTitledBorder(
activePanel == leftPanel ? "Left panel [ACTIVE]" : "Left panel"));
rightPanel.setBorder(BorderFactory.createTitledBorder(
activePanel == rightPanel ? "Right panel [ACTIVE]" : "Right panel"));
}
/**
* Switch between panels
*/
private void switchPanels() {
// Determine which panel currently (or recently) has focus by inspecting the focus owner.
java.awt.Component owner = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
boolean ownerInLeft = false;
boolean ownerInRight = false;
if (owner != null) {
Component p = owner;
while (p != null) {
if (p == leftPanel) {
ownerInLeft = true;
break;
}
if (p == rightPanel) {
ownerInRight = true;
break;
}
p = p.getParent();
}
}
if (ownerInLeft) {
rightPanel.requestFocusOnCurrentTab();
activePanel = rightPanel;
} else if (ownerInRight) {
leftPanel.requestFocusOnCurrentTab();
activePanel = leftPanel;
} else {
// Fallback to previous behavior based on activePanel
if (activePanel == leftPanel) {
rightPanel.requestFocusOnCurrentTab();
activePanel = rightPanel;
} else {
leftPanel.requestFocusOnCurrentTab();
activePanel = leftPanel;
}
}
// Update panel borders and force repaint so renderer can update focus colors
updateActivePanelBorder();
JTable lt = leftPanel.getFileTable();
JTable rt = rightPanel.getFileTable();
if (lt != null) lt.repaint();
if (rt != null) rt.repaint();
}
/**
* Public wrapper so child components (tabs) can request a panel switch.
*/
public void switchPanelsFromChild() {
switchPanels();
}
/**
* Attach TAB handling to switch panels
*/
private void addTabKeyHandler(JTable table) {
// Odstraníme standardní chování TAB ve Swing
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "switchPanel");
table.getActionMap().put("switchPanel", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
switchPanels();
}
});
// Add Ctrl+Tab handling at the table level
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK), "nextTab");
table.getActionMap().put("nextTab", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (activePanel != null) {
activePanel.nextTab();
}
}
});
// Přidáme F8 pro mazání s vyšší prioritou než defaultní Swing akce
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), "deleteFiles");
table.getActionMap().put("deleteFiles", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
deleteFiles();
}
});
// Also map Delete key to deleteFiles on the table level so it works when table has focus
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteFiles");
// Also map Shift+Delete on table level
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_DOWN_MASK), "deleteFiles");
}
/**
* Automatically focus command line when user starts typing on a table
*/
private void addCommandLineRedirect(JTable table) {
// Use InputMap/ActionMap for Ctrl+Enter and Ctrl+Shift+Enter as KeyListener might be bypassed by JTable
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "copyNameToCmd");
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), "copyPathToCmd");
table.getActionMap().put("copyNameToCmd", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
copyFocusedToCommandLine(false);
}
});
table.getActionMap().put("copyPathToCmd", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
copyFocusedToCommandLine(true);
}
});
table.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
// Printable characters only (exclude control keys like Enter, Backspace, Esc, Tab)
if (c != KeyEvent.CHAR_UNDEFINED && c != '\b' && c != '\n' && c != '\t' && c != 27) {
commandLine.requestFocusInWindow();
commandLine.setText(commandLine.getText() + c);
e.consume();
}
}
});
}
private void copyFocusedToCommandLine(boolean fullPath) {
FileItem focused = activePanel.getFocusedItem();
if (focused != null && !focused.getName().equals("..")) {
String current = commandLine.getText();
String toAdd = fullPath ? focused.getFile().getAbsolutePath() : focused.getName();
// If it contains spaces, wrap in quotes
if (toAdd.contains(" ")) {
toAdd = "\"" + toAdd + "\"";
}
if (!current.isEmpty() && !current.endsWith(" ")) {
commandLine.setText(current + " " + toAdd);
} else {
commandLine.setText(current + toAdd);
}
commandLine.requestFocusInWindow();
}
}
/**
* Copy selected files to the opposite panel
*/
private void copyFiles() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No files selected",
"Copy",
JOptionPane.INFORMATION_MESSAGE);
return;
}
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this,
String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Copy",
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback);
}, "Kopírování dokončeno", true, targetPanel);
}
}
/**
* Move selected files to the opposite panel
*/
private void moveFiles() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No files selected",
"Move",
JOptionPane.INFORMATION_MESSAGE);
return;
}
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this,
String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Move",
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback);
}, "Přesouvání dokončeno", false, activePanel, targetPanel);
}
}
/**
* Delete selected files
*/
private void deleteFiles() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No files selected",
"Delete",
JOptionPane.INFORMATION_MESSAGE);
return;
}
// remember current selection row so we can restore selection after deletion
JTable table = activePanel != null ? activePanel.getFileTable() : null;
final int rememberedRow = (table != null) ? table.getSelectedRow() : -1;
StringBuilder message = new StringBuilder("Really delete the following items?\n\n");
for (FileItem item : selectedItems) {
message.append(item.getName()).append("\n");
if (message.length() > 500) {
message.append("...");
break;
}
}
int result = JOptionPane.showConfirmDialog(this,
message.toString(),
"Mazání",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
performFileOperation((callback) -> {
FileOperations.delete(selectedItems, callback);
}, "Mazání dokončeno", false, activePanel);
// After deletion and refresh, restore selection: stay on same row if possible,
// otherwise move selection one row up.
SwingUtilities.invokeLater(() -> {
try {
JTable t = activePanel != null ? activePanel.getFileTable() : null;
if (t == null) return;
int rowCount = t.getRowCount();
if (rowCount == 0) return;
int targetRow = rememberedRow;
if (targetRow < 0) targetRow = 0;
if (targetRow >= rowCount) targetRow = rowCount - 1; // move up if needed
t.setRowSelectionInterval(targetRow, targetRow);
t.scrollRectToVisible(t.getCellRect(targetRow, 0, true));
} catch (Exception ignore) {}
});
}
}
/**
* Zip selected files
*/
private void zipFiles() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No files selected",
"Zip",
JOptionPane.INFORMATION_MESSAGE);
return;
}
String defaultName;
if (selectedItems.size() == 1) {
defaultName = selectedItems.get(0).getName();
} else {
defaultName = activePanel.getCurrentDirectory().getName();
if (defaultName == null || defaultName.isEmpty() || defaultName.equals("/") || defaultName.endsWith(":")) {
defaultName = "archive";
}
}
if (defaultName.contains(".")) {
int lastDot = defaultName.lastIndexOf('.');
if (lastDot > 0) {
defaultName = defaultName.substring(0, lastDot);
}
}
defaultName += ".zip";
String zipName = JOptionPane.showInputDialog(this, "Enter zip filename:", defaultName);
if (zipName == null || zipName.trim().isEmpty()) {
return;
}
if (!zipName.toLowerCase().endsWith(".zip")) {
zipName += ".zip";
}
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
File targetZip = new File(targetDir, zipName);
if (targetZip.exists()) {
int confirm = JOptionPane.showConfirmDialog(this,
"File already exists. Overwrite?", "Zip", JOptionPane.YES_NO_OPTION);
if (confirm != JOptionPane.YES_OPTION) {
return;
}
}
final File finalTargetZip = targetZip;
performFileOperation((callback) -> {
FileOperations.zip(selectedItems, finalTargetZip, callback);
}, "Zabaleno do " + zipName, false, targetPanel);
}
/**
* Unzip selected zip file
*/
private void unzipFiles() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No files selected",
"Unzip",
JOptionPane.INFORMATION_MESSAGE);
return;
}
File zipFile = selectedItems.get(0).getFile();
if (!zipFile.getName().toLowerCase().endsWith(".zip")) {
JOptionPane.showMessageDialog(this,
"Selected file is not a ZIP archive",
"Unzip",
JOptionPane.ERROR_MESSAGE);
return;
}
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this,
String.format("Unzip %s to:\n%s", zipFile.getName(), targetDir.getAbsolutePath()),
"Unzip",
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation((callback) -> {
FileOperations.unzip(zipFile, targetDir, callback);
}, "Rozbaleno do " + targetDir.getName(), false, targetPanel);
}
}
/**
* Rename selected file
*/
private void renameFile() {
// Start inline rename in the active panel (it will handle validation)
if (activePanel != null) {
try {
activePanel.startInlineRename();
} catch (Exception ex) {
// Fallback to dialog-based rename if inline fails for some reason
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.size() != 1) {
JOptionPane.showMessageDialog(this,
"Select one file to rename",
"Rename",
JOptionPane.INFORMATION_MESSAGE);
return;
}
FileItem item = selectedItems.get(0);
String newName = JOptionPane.showInputDialog(this,
"New name:",
item.getName());
if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) {
performFileOperation((callback) -> {
FileOperations.rename(item.getFile(), newName.trim());
}, "Přejmenování dokončeno", false, activePanel);
}
}
}
}
/**
* Create a new directory
*/
private void createNewDirectory() {
String dirName = JOptionPane.showInputDialog(this,
"New directory name:",
"New directory");
if (dirName != null && !dirName.trim().isEmpty()) {
performFileOperation((callback) -> {
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName.trim());
}, "Directory created", false, activePanel);
}
}
/**
* Show search dialog
*/
private void showSearchDialog() {
SearchDialog dialog = new SearchDialog(this, activePanel.getCurrentDirectory(), config);
dialog.setVisible(true);
}
/**
* Show the given file's parent directory in the panel that currently has focus
* and select the file in that panel.
*/
public void showFileInFocusedPanel(File file) {
if (file == null) return;
// Determine which panel currently has focus
java.awt.Component owner = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
FilePanel target = null;
if (owner != null) {
Component p = owner;
while (p != null) {
if (p == leftPanel) {
target = leftPanel;
break;
}
if (p == rightPanel) {
target = rightPanel;
break;
}
p = p.getParent();
}
}
if (target == null) target = activePanel != null ? activePanel : leftPanel;
final FilePanel chosen = target;
final File parentDir = file.getParentFile();
if (parentDir == null) return;
// Load directory and then select item by name on the EDT
SwingUtilities.invokeLater(() -> {
chosen.loadDirectory(parentDir);
// mark this panel active and refresh borders
activePanel = chosen;
updateActivePanelBorder();
// After loading, select the file name
SwingUtilities.invokeLater(() -> {
FilePanelTab tab = chosen.getCurrentTab();
if (tab != null) {
tab.selectItem(file.getName());
tab.getFileTable().requestFocusInWindow();
}
});
});
}
/**
* Show file in internal viewer
*/
private void viewFile() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
return;
}
FileItem item = selectedItems.get(0);
if (item.isDirectory() || item.getName().equals("..")) {
return;
}
File file = item.getFile();
// Removed previous 10 MB limit: allow opening large files in the viewer (paged hex will stream large binaries).
FileEditor viewer = new FileEditor(this, file, config, true);
viewer.setVisible(true);
}
/**
* Open file in internal editor
*/
private void editFile() {
List<FileItem> selectedItems = activePanel.getSelectedItems();
if (selectedItems.isEmpty()) {
return;
}
FileItem item = selectedItems.get(0);
if (item.isDirectory() || item.getName().equals("..")) {
return;
}
File file = item.getFile();
// If an external editor is configured, try launching it with the file path
String ext = config.getExternalEditorPath();
if (ext != null && !ext.trim().isEmpty()) {
try {
java.util.List<String> cmd = new java.util.ArrayList<>();
cmd.add(ext);
cmd.add(file.getAbsolutePath());
new ProcessBuilder(cmd).start();
return;
} catch (Exception ex) {
// Fall back to internal editor if external fails
JOptionPane.showMessageDialog(this,
"Nelze spustit externí editor: " + ex.getMessage() + "\nPoužije se interní editor.",
"Chyba", JOptionPane.ERROR_MESSAGE);
}
}
// Removed previous 10 MB limit: allow opening large files in the editor. The editor may still choose hex/paged mode for large binaries.
FileEditor editor = new FileEditor(this, file, config, false);
editor.setVisible(true);
}
/**
* Refresh both panels
*/
private void refreshPanels() {
// Refresh je nyní automatický při změnách
// Pokud je potřeba manuální refresh, můžeme zavolat loadDirectory
if (leftPanel.getCurrentDirectory() != null) {
leftPanel.loadDirectory(leftPanel.getCurrentDirectory());
}
if (rightPanel.getCurrentDirectory() != null) {
rightPanel.loadDirectory(rightPanel.getCurrentDirectory());
}
}
/**
* Set the view mode for the active panel
*/
private void setActiveViewMode(ViewMode mode) {
if (activePanel != null) {
activePanel.setViewMode(mode);
}
}
/**
* Add a new tab to the active panel
*/
private void addNewTabToActivePanel() {
if (activePanel != null) {
File currentDir = activePanel.getCurrentDirectory();
String path = currentDir != null ? currentDir.getAbsolutePath() : System.getProperty("user.home");
activePanel.addNewTab(path);
}
}
/**
* Close the current tab in the active panel
*/
private void closeCurrentTabInActivePanel() {
if (activePanel != null) {
activePanel.closeCurrentTab();
}
}
/**
* Open terminal in the current directory
*/
private void openTerminal() {
File currentDir = activePanel.getCurrentDirectory();
if (currentDir == null) {
currentDir = new File(System.getProperty("user.home"));
}
try {
String osName = System.getProperty("os.name").toLowerCase();
ProcessBuilder pb = null;
if (osName.contains("win")) {
// Windows
pb = new ProcessBuilder("cmd.exe", "/c", "start", "cmd.exe");
} else if (osName.contains("mac")) {
// macOS
pb = new ProcessBuilder("open", "-a", "Terminal", currentDir.getAbsolutePath());
} else {
// Linux and other Unix-like systems
// Try common terminal emulators with working directory arguments
String[] terminals = {"gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal", "xterm"};
for (String terminal : terminals) {
try {
Process p = Runtime.getRuntime().exec(new String[]{"which", terminal});
if (p.waitFor() == 0) {
if (terminal.equals("gnome-terminal") || terminal.equals("xfce4-terminal") || terminal.equals("mate-terminal")) {
pb = new ProcessBuilder(terminal, "--working-directory=" + currentDir.getAbsolutePath());
} else if (terminal.equals("konsole")) {
pb = new ProcessBuilder(terminal, "--workdir", currentDir.getAbsolutePath());
} else {
pb = new ProcessBuilder(terminal);
}
break;
}
} catch (Exception e) {
// Try next terminal
}
}
if (pb == null) {
// Fallback to xterm
pb = new ProcessBuilder("xterm");
}
}
if (pb != null) {
pb.directory(currentDir);
pb.start();
}
} catch (Exception e) {
JOptionPane.showMessageDialog(this,
"Chyba při otevírání terminálu: " + e.getMessage(),
"Chyba",
JOptionPane.ERROR_MESSAGE);
}
}
private void executeCommand(String command) {
if (command == null || command.trim().isEmpty()) {
activePanel.getFileTable().requestFocusInWindow();
return;
}
File currentDir = activePanel.getCurrentDirectory();
if (currentDir == null) {
currentDir = new File(System.getProperty("user.home"));
}
try {
String osName = System.getProperty("os.name").toLowerCase();
ProcessBuilder pb;
if (osName.contains("win")) {
// Windows: cmd /c command && pause
pb = new ProcessBuilder("cmd.exe", "/c", "start", "cmd.exe", "/k", command);
} else if (osName.contains("mac")) {
// macOS: Open terminal and execute
String appleScript = String.format(
"tell application \"Terminal\" to do script \"cd '%s' && %s\"",
currentDir.getAbsolutePath(), command.replace("\"", "\\\"")
);
pb = new ProcessBuilder("osascript", "-e", appleScript);
} else {
// Linux: Try common terminals with -e or --command
String[] terminals = {"gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal", "xterm"};
pb = null;
for (String terminal : terminals) {
try {
Process p = Runtime.getRuntime().exec(new String[]{"which", terminal});
if (p.waitFor() == 0) {
if (terminal.equals("gnome-terminal") || terminal.equals("xfce4-terminal") || terminal.equals("mate-terminal")) {
pb = new ProcessBuilder(terminal, "--working-directory=" + currentDir.getAbsolutePath(), "--", "bash", "-c", command + "; echo; echo 'Press Enter to close...'; read");
} else if (terminal.equals("konsole")) {
pb = new ProcessBuilder(terminal, "--workdir", currentDir.getAbsolutePath(), "-e", "bash", "-c", command + "; echo; echo 'Press Enter to close...'; read");
} else {
pb = new ProcessBuilder(terminal, "-e", "bash -c \"" + command + "; echo; echo 'Press Enter to close...'; read\"");
}
break;
}
} catch (Exception e) {
// try next
}
}
if (pb == null) {
pb = new ProcessBuilder("xterm", "-e", "bash -c \"" + command + "; echo; echo 'Press Enter to close...'; read\"");
}
}
if (pb != null) {
pb.directory(currentDir);
pb.start();
commandLine.setText(""); // Clear after execution
activePanel.getFileTable().requestFocusInWindow();
}
} catch (Exception e) {
JOptionPane.showMessageDialog(this,
"Chyba při spouštění příkazu: " + e.getMessage(),
"Chyba",
JOptionPane.ERROR_MESSAGE);
}
}
/**
* Show About dialog
*/
private void showAboutDialog() {
JOptionPane.showMessageDialog(this,
"KF File Manager 1.0\n\n" +
"Two-panel file manager\n" +
"Java 11\n\n" +
"Keyboard shortcuts:\n" +
"F5 - Copy\n" +
"Alt+F5 - Zip\n" +
"Alt+F9 - Unzip\n" +
"F6 - Move\n" +
"F7 - New directory\n" +
"F8 - Delete\n" +
"F9 - Open terminal\n" +
"Shift+F6 - Rename\n" +
"TAB - Switch panel\n" +
"Ctrl+F - Search\n" +
"Alt+O - Settings\n" +
"Enter - Open directory\n" +
"Backspace - Parent directory",
"About",
JOptionPane.INFORMATION_MESSAGE);
}
/**
* Execute file operation with error handling
*/
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, FilePanel... panelsToRefresh) {
ProgressDialog progressDialog = new ProgressDialog(this, "Operační systém");
progressDialog.setDisplayAsBytes(showBytes);
FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
@Override
public void onProgress(long current, long total, String currentFile) {
progressDialog.updateProgress(current, total, currentFile);
}
@Override
public boolean isCancelled() {
return progressDialog.isCancelled();
}
};
// Run operation in a background thread
new Thread(() -> {
try {
operation.execute(callback);
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
for (FilePanel panel : panelsToRefresh) {
if (panel.getCurrentDirectory() != null) {
panel.loadDirectory(panel.getCurrentDirectory());
}
}
if (callback.isCancelled()) {
JOptionPane.showMessageDialog(MainWindow.this, "Operace byla přerušena uživatelem.");
}
});
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
JOptionPane.showMessageDialog(MainWindow.this,
"Chyba: " + e.getMessage(),
"Chyba",
JOptionPane.ERROR_MESSAGE);
});
}
}).start();
progressDialog.setVisible(true);
}
/**
* Show drive selector for left panel
*/
private void selectDriveForLeftPanel() {
// Open the drive dropdown in the left panel
if (leftPanel != null) leftPanel.showDrivePopup();
}
/**
* Show drive selector for right panel
*/
private void selectDriveForRightPanel() {
// Open the drive dropdown in the right panel
if (rightPanel != null) rightPanel.showDrivePopup();
}
/**
* Save configuration and exit application
*/
private void saveConfigAndExit() {
// Save window state
config.saveWindowState(this);
// Save current panel paths (for backward compatibility)
if (leftPanel.getCurrentDirectory() != null) {
config.setLeftPanelPath(leftPanel.getCurrentDirectory().getAbsolutePath());
}
if (rightPanel.getCurrentDirectory() != null) {
config.setRightPanelPath(rightPanel.getCurrentDirectory().getAbsolutePath());
}
// Save open tabs + view modes and selected index for both panels
try {
java.util.List<String> leftPaths = leftPanel.getTabPaths();
java.util.List<String> leftModes = leftPanel.getTabViewModes();
int leftSelected = leftPanel.getSelectedTabIndex();
config.saveLeftPanelTabs(leftPaths, leftModes, leftSelected);
} catch (Exception ex) {
// ignore
}
try {
java.util.List<String> rightPaths = rightPanel.getTabPaths();
java.util.List<String> rightModes = rightPanel.getTabViewModes();
int rightSelected = rightPanel.getSelectedTabIndex();
config.saveRightPanelTabs(rightPaths, rightModes, rightSelected);
} catch (Exception ex) {
// ignore
}
// Uložit konfiguraci do souboru
config.saveConfig();
// Exit application
System.exit(0);
}
@FunctionalInterface
private interface FileOperation {
void execute(FileOperations.ProgressCallback callback) throws Exception;
}
}