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 paths = new java.util.ArrayList<>(); java.util.List 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 paths = new java.util.ArrayList<>(); java.util.List 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 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 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 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 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 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 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 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 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 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 leftPaths = leftPanel.getTabPaths(); java.util.List leftModes = leftPanel.getTabViewModes(); int leftSelected = leftPanel.getSelectedTabIndex(); config.saveLeftPanelTabs(leftPaths, leftModes, leftSelected); } catch (Exception ex) { // ignore } try { java.util.List rightPaths = rightPanel.getTabPaths(); java.util.List 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; } }