package com.kfmanager.ui; import com.kfmanager.MainApp; import com.kfmanager.config.AppConfig; import com.kfmanager.model.FileItem; import com.kfmanager.service.FileOperations; import javax.swing.*; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; 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 JToolBar toolBar; private JComboBox commandLine; private JLabel cmdLabel; private AppConfig config; public MainWindow() { super("KF Manager v" + MainApp.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()); leftPanel.setOnDirectoryChangedAll(() -> updateCommandLinePrompt()); // Load and set ViewMode for left panel try { ViewMode leftViewMode = ViewMode.valueOf(config.getLeftPanelViewMode()); leftPanel.setViewMode(leftViewMode); } catch (IllegalArgumentException e) { // Default value FULL is already set } // 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()); rightPanel.setOnDirectoryChangedAll(() -> updateCommandLinePrompt()); // Load and set ViewMode for right panel try { ViewMode rightViewMode = ViewMode.valueOf(config.getRightPanelViewMode()); rightPanel.setViewMode(rightViewMode); } catch (IllegalArgumentException e) { // Default value FULL is already set } mainPanel.add(leftPanel); mainPanel.add(rightPanel); add(mainPanel, BorderLayout.CENTER); // Set left panel as active by default activePanel = leftPanel; updateActivePanelBorder(); updateCommandLinePrompt(); // 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 } // Global focus listener to track which panel is active based on focused component KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("permanentFocusOwner", evt -> { Component focused = (Component) evt.getNewValue(); if (focused != null) { if (SwingUtilities.isDescendingFrom(focused, leftPanel)) { activePanel = leftPanel; updateActivePanelBorder(); leftPanel.getFileTable().repaint(); rightPanel.getFileTable().repaint(); updateCommandLinePrompt(); } else if (SwingUtilities.isDescendingFrom(focused, rightPanel)) { activePanel = rightPanel; updateActivePanelBorder(); leftPanel.getFileTable().repaint(); rightPanel.getFileTable().repaint(); updateCommandLinePrompt(); } } }); // Focus listeners to track active panel and ensure selection leftPanel.getFileTable().addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { // Ensure some row is selected JTable leftTable = leftPanel.getFileTable(); if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) { leftTable.setRowSelectionInterval(0, 0); } } @Override public void focusLost(FocusEvent e) { // Repaint on focus loss leftPanel.getFileTable().repaint(); } }); rightPanel.getFileTable().addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { // Ensure some row is selected JTable rightTable = rightPanel.getFileTable(); if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) { rightTable.setRowSelectionInterval(0, 0); } } @Override public void focusLost(FocusEvent e) { // Repaint on focus loss 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)); cmdLabel = new JLabel(System.getProperty("user.name") + ":" + (activePanel != null ? activePanel.getCurrentDirectory().getAbsolutePath() : "") + ">"); cmdLabel.setFont(new Font("Monospaced", Font.BOLD, 12)); cmdPanel.add(cmdLabel, BorderLayout.WEST); commandLine = new JComboBox<>(); commandLine.setEditable(true); commandLine.setFont(new Font("Monospaced", Font.PLAIN, 12)); // Handle the editor component (usually a JTextField) Component editorComp = commandLine.getEditor().getEditorComponent(); if (editorComp instanceof JTextField) { JTextField tf = (JTextField) editorComp; tf.setFocusTraversalKeysEnabled(false); tf.addActionListener(e -> executeCommand(tf.getText())); // Let the panels catch focus back if user presses ESC or TAB in command line tf.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { tf.setText(""); activePanel.getFileTable().requestFocusInWindow(); e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_TAB) { activePanel.getFileTable().requestFocusInWindow(); e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_E && e.isControlDown()) { showCommandLineHistory(); e.consume(); } } }); } cmdPanel.add(commandLine, BorderLayout.CENTER); // Load history from config java.util.List history = config.getCommandLineHistory(); for (int i = 0; i < history.size(); i++) { commandLine.addItem(history.get(i)); } commandLine.getEditor().setItem(""); // Ensure it starts empty 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() { if (toolBar == null) { toolBar = new JToolBar(); toolBar.setFloatable(false); toolBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); add(toolBar, BorderLayout.NORTH); // Enable Drag and Drop for adding shortcuts toolBar.setTransferHandler(new TransferHandler() { @Override public boolean canImport(TransferSupport support) { return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor); } @Override @SuppressWarnings("unchecked") public boolean importData(TransferSupport support) { if (!canImport(support)) return false; try { List files = (List) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); if (files != null && !files.isEmpty()) { showAddToolbarShortcutDialog(files.get(0)); } return true; } catch (Exception e) { return false; } } }); } toolBar.removeAll(); // 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(); // Load custom shortcuts from config List shortcuts = config.getToolbarShortcuts(); int btnSize = config.getToolbarButtonSize(); int iconSize = config.getToolbarIconSize(); for (AppConfig.ToolbarShortcut s : shortcuts) { JButton btn = new JButton(); btn.setPreferredSize(new Dimension(btnSize, btnSize)); btn.setMinimumSize(new Dimension(btnSize, btnSize)); btn.setMaximumSize(new Dimension(btnSize, btnSize)); boolean hasIcon = false; if (s.iconPath != null && !s.iconPath.isEmpty()) { try { File iconFile = new File(s.iconPath); if (iconFile.exists()) { btn.setIcon(new ImageIcon(new ImageIcon(s.iconPath).getImage().getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH))); hasIcon = true; } } catch (Exception ignore) {} } // If no icon found, use the label text, otherwise use label as tooltip if (!hasIcon) { btn.setText(s.label); } btn.setToolTipText(s.label + " (" + s.command + ")"); btn.setFocusable(false); btn.addActionListener(e -> executeNative(s.command, s.workingDir)); // Context menu for Edit/Delete JPopupMenu shortcutPopup = new JPopupMenu(); JMenuItem editItem = new JMenuItem("Edit"); editItem.addActionListener(ae -> showEditToolbarShortcutDialog(s)); JMenuItem deleteItem = new JMenuItem("Delete"); deleteItem.addActionListener(ae -> { int choice = JOptionPane.showConfirmDialog(MainWindow.this, "Remove this shortcut?", "Toolbar", JOptionPane.YES_NO_OPTION); if (choice == JOptionPane.YES_OPTION) { removeToolbarShortcut(s); } }); shortcutPopup.add(editItem); shortcutPopup.add(deleteItem); btn.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { shortcutPopup.show(e.getComponent(), e.getX(), e.getY()); } } @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { shortcutPopup.show(e.getComponent(), e.getX(), e.getY()); } } }); toolBar.add(btn); } toolBar.revalidate(); toolBar.repaint(); } private void showAddToolbarShortcutDialog(File file) { showToolbarShortcutEditor(null, file); } private void showEditToolbarShortcutDialog(AppConfig.ToolbarShortcut shortcut) { showToolbarShortcutEditor(shortcut, null); } private void showToolbarShortcutEditor(AppConfig.ToolbarShortcut existing, File file) { String initialLabel = (existing != null) ? existing.label : (file != null ? file.getName() : ""); String initialCmd = (existing != null) ? existing.command : (file != null ? file.getAbsolutePath() : ""); String initialWorkDir = (existing != null) ? existing.workingDir : (file != null ? (file.isDirectory() ? file.getAbsolutePath() : file.getParent()) : ""); String initialIcon = (existing != null) ? existing.iconPath : ""; JTextField labelField = new JTextField(initialLabel); JTextField cmdField = new JTextField(initialCmd); JTextField workingDirField = new JTextField(initialWorkDir); JTextField iconField = new JTextField(initialIcon); JPanel panel = new JPanel(new GridLayout(0, 1)); panel.add(new JLabel("Label:")); panel.add(labelField); panel.add(new JLabel("Command:")); panel.add(cmdField); panel.add(new JLabel("Working directory:")); JPanel workDirPanel = new JPanel(new BorderLayout()); workDirPanel.add(workingDirField, BorderLayout.CENTER); JButton browseWorkDir = new JButton("..."); browseWorkDir.addActionListener(e -> { JFileChooser chooser = new JFileChooser(workingDirField.getText()); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { workingDirField.setText(chooser.getSelectedFile().getAbsolutePath()); } }); workDirPanel.add(browseWorkDir, BorderLayout.EAST); panel.add(workDirPanel); panel.add(new JLabel("Icon path (optional):")); JPanel iconPanel = new JPanel(new BorderLayout()); iconPanel.add(iconField, BorderLayout.CENTER); JButton browseIcon = new JButton("..."); browseIcon.addActionListener(e -> { String startPath = iconField.getText().trim(); if (startPath.isEmpty()) { startPath = workingDirField.getText().trim(); if (startPath.isEmpty()) { startPath = cmdField.getText().trim(); } } JFileChooser chooser = new JFileChooser(startPath); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { iconField.setText(chooser.getSelectedFile().getAbsolutePath()); } }); iconPanel.add(browseIcon, BorderLayout.EAST); panel.add(iconPanel); String title = (existing != null) ? "Edit Toolbar Shortcut" : "Add Toolbar Shortcut"; int result = JOptionPane.showConfirmDialog(this, panel, title, JOptionPane.OK_CANCEL_OPTION); if (result == JOptionPane.OK_OPTION) { String label = labelField.getText().trim(); String command = cmdField.getText().trim(); String icon = iconField.getText().trim(); String workDir = workingDirField.getText().trim(); if (!command.isEmpty()) { if (label.isEmpty()) label = (file != null ? file.getName() : "Shortcut"); List shortcuts = config.getToolbarShortcuts(); if (existing != null) { // Find and replace for (int i = 0; i < shortcuts.size(); i++) { AppConfig.ToolbarShortcut s = shortcuts.get(i); // Identify by original command/label if they haven't changed yet in memory if (s.command.equals(existing.command) && s.label.equals(existing.label)) { shortcuts.set(i, new AppConfig.ToolbarShortcut(command, label, icon, workDir)); break; } } } else { shortcuts.add(new AppConfig.ToolbarShortcut(command, label, icon, workDir)); } config.saveToolbarShortcuts(shortcuts); createToolBar(); // refresh } } } private void removeToolbarShortcut(AppConfig.ToolbarShortcut shortcut) { List shortcuts = config.getToolbarShortcuts(); shortcuts.removeIf(s -> s.command.equals(shortcut.command) && s.label.equals(shortcut.label)); config.saveToolbarShortcuts(shortcuts); createToolBar(); } /** * 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); } // Refresh toolbar if sizes changed SwingUtilities.invokeLater(() -> createToolBar()); // 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 - Viewer 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.getEditor().setItem(""); 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); // Ctrl+E - Command line history rootPane.registerKeyboardAction(e -> showCommandLineHistory(), KeyStroke.getKeyStroke(KeyEvent.VK_E, 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(); updateCommandLinePrompt(); } /** * Public wrapper so child components (tabs) can request a panel switch. */ public void switchPanelsFromChild() { switchPanels(); } private void updateCommandLinePrompt() { if (cmdLabel == null) return; FilePanel active = activePanel; if (active == null) return; String path = active.getCurrentPath(); cmdLabel.setText(path + ">"); } /** * Attach TAB handling to switch panels */ private void addTabKeyHandler(JTable table) { // Remove standard Swing TAB behavior 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(); } } }); // Add F8 for deleting with higher priority than default Swing actions 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(); String current = commandLine.getEditor().getItem().toString(); commandLine.getEditor().setItem(current + c); e.consume(); } } }); } private void copyFocusedToCommandLine(boolean fullPath) { FileItem focused = activePanel.getFocusedItem(); if (focused != null && !focused.getName().equals("..")) { String current = commandLine.getEditor().getItem().toString(); 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.getEditor().setItem(current + " " + toAdd); } else { commandLine.getEditor().setItem(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); }, "Copy completed", true, targetPanel); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } /** * 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); }, "Move completed", false, activePanel, targetPanel); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } /** * 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(), "Delete", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (result == JOptionPane.YES_OPTION) { performFileOperation((callback) -> { FileOperations.delete(selectedItems, callback); }, "Delete completed", 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)); t.requestFocusInWindow(); } catch (Exception ignore) {} }); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } /** * 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()) { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } 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) { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } return; } } final File finalTargetZip = targetZip; performFileOperation((callback) -> { FileOperations.zip(selectedItems, finalTargetZip, callback); }, "Zipped into " + 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); }, "Unzipped into " + targetDir.getName(), false, targetPanel); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } /** * 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()); }, "Rename completed", false, activePanel); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } } } /** * 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); } else { if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } } /** * 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, "Could not start external editor: " + ex.getMessage() + "\nUsing internal editor.", "Error", 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 is now automatic upon changes // If manual refresh is needed, we can call 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); } } /** * Show history of command line */ private void showCommandLineHistory() { if (commandLine != null && commandLine.getItemCount() > 0) { commandLine.requestFocusInWindow(); if (!commandLine.isPopupVisible()) { commandLine.setSelectedIndex(0); commandLine.showPopup(); } else { int count = commandLine.getItemCount(); int current = commandLine.getSelectedIndex(); // If index is -1 or invalid, start from 0, otherwise go to next int nextIndex = (current < 0) ? 0 : (current + 1) % count; commandLine.setSelectedIndex(nextIndex); } } } /** * 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, "Error opening terminal: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private void executeCommand(String command) { if (command == null || command.trim().isEmpty()) { activePanel.getFileTable().requestFocusInWindow(); return; } // Add to history addCommandToHistory(command.trim()); // Final prompt update after command execution (path might have changed) updateCommandLinePrompt(); // Execute natively, not via bash wrapper executeNative(command.trim(), null); // Clear after execution and return focus Component editorComp = commandLine.getEditor().getEditorComponent(); if (editorComp instanceof JTextField) { ((JTextField) editorComp).setText(""); } else { commandLine.setSelectedItem(""); } activePanel.getFileTable().requestFocusInWindow(); } private void executeNative(String command, String workingDir) { if (command == null || command.trim().isEmpty()) return; File currentDir; if (workingDir != null && !workingDir.trim().isEmpty()) { currentDir = new File(workingDir); } else { currentDir = activePanel.getCurrentDirectory(); } if (currentDir == null || !currentDir.exists()) { currentDir = new File(System.getProperty("user.home")); } try { // Check if it's a file path that exists (and not a complex command) if (!command.contains(" ") || (command.startsWith("\"") && command.endsWith("\"") && !command.substring(1, command.length()-1).contains("\""))) { String path = command.startsWith("\"") ? command.substring(1, command.length()-1) : command; File file = new File(path); if (file.exists()) { // Start executable files directly, otherwise use Desktop.open if (file.canExecute() && !file.isDirectory()) { new ProcessBuilder(file.getAbsolutePath()).directory(currentDir).start(); return; } if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { Desktop.getDesktop().open(file); return; } } } // Otherwise try running as a native process List cmdList = parseCommand(command); try { new ProcessBuilder(cmdList).directory(currentDir).start(); } catch (IOException ex) { // Fallback for Linux/macOS: try via shell String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("linux") || osName.contains("mac")) { new ProcessBuilder("sh", "-c", command).directory(currentDir).start(); } else { throw ex; } } } catch (Exception e) { JOptionPane.showMessageDialog(this, "Error executing native command: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private List parseCommand(String command) { List list = new ArrayList<>(); StringBuilder sb = new StringBuilder(); boolean inQuotes = false; for (int i = 0; i < command.length(); i++) { char c = command.charAt(i); if (c == '\"') { inQuotes = !inQuotes; } else if (c == ' ' && !inQuotes) { if (sb.length() > 0) { list.add(sb.toString()); sb.setLength(0); } } else { sb.append(c); } } if (sb.length() > 0) { list.add(sb.toString()); } return list; } private void addCommandToHistory(String command) { if (command == null || command.isEmpty()) return; // Remove if already exists to move it to the top for (int i = 0; i < commandLine.getItemCount(); i++) { if (command.equals(commandLine.getItemAt(i))) { commandLine.removeItemAt(i); break; } } commandLine.insertItemAt(command, 0); // We don't necessarily want to select it here as it might interfere with the editor state } /** * 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, "File Operation"); 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 (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } if (callback.isCancelled()) { JOptionPane.showMessageDialog(MainWindow.this, "Operation was cancelled by user."); if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } } }); } catch (Exception e) { SwingUtilities.invokeLater(() -> { progressDialog.dispose(); JOptionPane.showMessageDialog(MainWindow.this, "Error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); if (activePanel != null && activePanel.getFileTable() != null) { activePanel.getFileTable().requestFocusInWindow(); } }); } }).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 } // Save command history java.util.List cmdHistory = new java.util.ArrayList<>(); for (int i = 0; i < commandLine.getItemCount(); i++) { cmdHistory.add(commandLine.getItemAt(i)); } config.saveCommandLineHistory(cmdHistory); // Save configuration to file config.saveConfig(); // Exit application System.exit(0); } @FunctionalInterface private interface FileOperation { void execute(FileOperations.ProgressCallback callback) throws Exception; } }