2380 lines
95 KiB
Java
2380 lines
95 KiB
Java
package cz.kamma.kfmanager.ui;
|
|
|
|
import cz.kamma.kfmanager.MainApp;
|
|
import cz.kamma.kfmanager.config.AppConfig;
|
|
import cz.kamma.kfmanager.model.FileItem;
|
|
import cz.kamma.kfmanager.service.FileOperations;
|
|
import cz.kamma.kfmanager.service.FileOperationQueue;
|
|
|
|
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<String> commandLine;
|
|
private JLabel cmdLabel;
|
|
private AppConfig config;
|
|
private Timer autoRefreshTimer;
|
|
|
|
public MainWindow() {
|
|
super("KF Manager v" + MainApp.APP_VERSION);
|
|
|
|
// Set application icon
|
|
loadAppIcon();
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
// Refresh panels when application gains focus
|
|
addWindowFocusListener(new WindowAdapter() {
|
|
@Override
|
|
public void windowGainedFocus(WindowEvent e) {
|
|
refreshPanels();
|
|
}
|
|
});
|
|
|
|
// After start, set focus and selection to the active panel
|
|
String initialActiveSide = config.getActivePanel();
|
|
SwingUtilities.invokeLater(() -> {
|
|
if ("right".equalsIgnoreCase(initialActiveSide)) {
|
|
activePanel = rightPanel;
|
|
rightPanel.requestFocusOnCurrentTab();
|
|
} else {
|
|
activePanel = leftPanel;
|
|
leftPanel.requestFocusOnCurrentTab();
|
|
}
|
|
updateActivePanelBorder();
|
|
updateCommandLinePrompt();
|
|
});
|
|
|
|
// Setup auto-refresh timer from config
|
|
updateAutoRefreshTimer();
|
|
}
|
|
|
|
private void loadAppIcon() {
|
|
try {
|
|
java.net.URL iconURL = MainWindow.class.getResource("/icon.png");
|
|
if (iconURL != null) {
|
|
ImageIcon img = new ImageIcon(iconURL);
|
|
setIconImage(img.getImage());
|
|
}
|
|
} catch (Exception e) {
|
|
System.err.println("Could not load icon: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
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);
|
|
// 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, false);
|
|
} 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);
|
|
// 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, false);
|
|
} catch (IllegalArgumentException e) {
|
|
// Default value FULL is already set
|
|
}
|
|
|
|
mainPanel.add(leftPanel);
|
|
mainPanel.add(rightPanel);
|
|
|
|
add(mainPanel, BorderLayout.CENTER);
|
|
|
|
// Restore active panel from configuration
|
|
String savedActive = config.getActivePanel();
|
|
if ("right".equalsIgnoreCase(savedActive)) {
|
|
activePanel = rightPanel;
|
|
} else {
|
|
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<String> paths = new java.util.ArrayList<>();
|
|
java.util.List<String> modes = new java.util.ArrayList<>();
|
|
java.util.List<String> focusedItems = 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));
|
|
focusedItems.add(config.getLeftPanelTabFocusedItem(i));
|
|
}
|
|
int sel = config.getLeftPanelSelectedIndex();
|
|
leftPanel.restoreTabs(paths, modes, focusedItems, sel, false);
|
|
}
|
|
} catch (Exception ex) {
|
|
// ignore and keep default
|
|
}
|
|
|
|
try {
|
|
int rightCount = config.getRightPanelTabCount();
|
|
if (rightCount > 0) {
|
|
java.util.List<String> paths = new java.util.ArrayList<>();
|
|
java.util.List<String> modes = new java.util.ArrayList<>();
|
|
java.util.List<String> focusedItems = 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));
|
|
focusedItems.add(config.getRightPanelTabFocusedItem(i));
|
|
}
|
|
int sel = config.getRightPanelSelectedIndex();
|
|
rightPanel.restoreTabs(paths, modes, focusedItems, sel, false);
|
|
}
|
|
} 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) {
|
|
activePanel = leftPanel;
|
|
updateActivePanelBorder();
|
|
updateCommandLinePrompt();
|
|
// 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) {
|
|
activePanel = rightPanel;
|
|
updateActivePanelBorder();
|
|
updateCommandLinePrompt();
|
|
// 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()));
|
|
|
|
// Enable standard clipboard operations (Cut, Copy, Paste) even if not focused initially
|
|
tf.getComponentPopupMenu(); // Ensure it has a menu or at least default actions works
|
|
|
|
// Context menu with Clipboard operations
|
|
JPopupMenu clipboardMenu = new JPopupMenu();
|
|
Action cutAction = new javax.swing.text.DefaultEditorKit.CutAction();
|
|
cutAction.putValue(Action.NAME, "Cut");
|
|
clipboardMenu.add(cutAction);
|
|
|
|
Action copyAction = new javax.swing.text.DefaultEditorKit.CopyAction();
|
|
copyAction.putValue(Action.NAME, "Copy");
|
|
clipboardMenu.add(copyAction);
|
|
|
|
Action pasteAction = new javax.swing.text.DefaultEditorKit.PasteAction();
|
|
pasteAction.putValue(Action.NAME, "Paste");
|
|
clipboardMenu.add(pasteAction);
|
|
|
|
clipboardMenu.addSeparator();
|
|
|
|
Action selectAllAction = new AbstractAction("Select All") {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
tf.selectAll();
|
|
}
|
|
};
|
|
clipboardMenu.add(selectAllAction);
|
|
|
|
tf.setComponentPopupMenu(clipboardMenu);
|
|
|
|
// 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("");
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
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<String> 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();
|
|
|
|
// Request focus for the active panel
|
|
SwingUtilities.invokeLater(() -> {
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
activePanel.getFileTable().requestFocusInWindow();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
|
|
if (files != null && !files.isEmpty()) {
|
|
showAddToolbarShortcutDialog(files.get(0));
|
|
}
|
|
return true;
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
toolBar.removeAll();
|
|
|
|
// Refresh button
|
|
JButton btnRefresh = new JButton("↻");
|
|
btnRefresh.setToolTipText("Refresh active panel");
|
|
btnRefresh.setFocusable(false);
|
|
btnRefresh.addActionListener(e -> {
|
|
if (activePanel != null && activePanel.getCurrentDirectory() != null) {
|
|
activePanel.refresh(true);
|
|
}
|
|
});
|
|
toolBar.add(btnRefresh);
|
|
toolBar.addSeparator();
|
|
|
|
// Button for BRIEF mode
|
|
JButton btnBrief = new JButton("☰");
|
|
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("▤");
|
|
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<AppConfig.ToolbarShortcut> shortcuts = config.getToolbarShortcuts();
|
|
int btnSize = config.getToolbarButtonSize();
|
|
int iconSize = config.getToolbarIconSize();
|
|
|
|
// Group shortcuts: directories will go to the right, others stay on the left
|
|
List<JButton> leftShortcuts = new ArrayList<>();
|
|
List<JButton> rightShortcuts = new ArrayList<>();
|
|
|
|
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;
|
|
boolean isDirectory = false;
|
|
File target = new File(s.command);
|
|
if (target.exists() && target.isDirectory()) {
|
|
isDirectory = true;
|
|
}
|
|
|
|
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 custom icon, try to use the same icons as in file panels
|
|
if (!hasIcon) {
|
|
try {
|
|
if (target.exists()) {
|
|
Icon customIcon;
|
|
if (target.isDirectory()) {
|
|
customIcon = new ColoredFolderIcon(config.getFolderColor(), s.label);
|
|
} else {
|
|
customIcon = new FileSpecificIcon(FileSpecificIcon.getFileType(target.getName()));
|
|
}
|
|
|
|
// Scale custom icon to toolbar size
|
|
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(
|
|
customIcon.getIconWidth(), customIcon.getIconHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
|
java.awt.Graphics g = img.getGraphics();
|
|
customIcon.paintIcon(null, g, 0, 0);
|
|
g.dispose();
|
|
btn.setIcon(new ImageIcon(img.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 -> {
|
|
// If command is a directory that exists, change active panel directory
|
|
File dir = new File(s.command);
|
|
if (dir.exists() && dir.isDirectory()) {
|
|
if (activePanel != null) {
|
|
activePanel.loadDirectory(dir);
|
|
}
|
|
} else {
|
|
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());
|
|
}
|
|
}
|
|
});
|
|
|
|
if (isDirectory) {
|
|
rightShortcuts.add(btn);
|
|
} else {
|
|
leftShortcuts.add(btn);
|
|
}
|
|
}
|
|
|
|
// Add non-directory shortcuts to the left
|
|
for (JButton b : leftShortcuts) {
|
|
toolBar.add(b);
|
|
}
|
|
|
|
// Push directory shortcuts to the right
|
|
toolBar.add(javax.swing.Box.createHorizontalGlue());
|
|
|
|
// Add directory shortcuts to the right
|
|
for (JButton b : rightShortcuts) {
|
|
toolBar.add(b);
|
|
}
|
|
|
|
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<AppConfig.ToolbarShortcut> 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<AppConfig.ToolbarShortcut> 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_F7, InputEvent.ALT_DOWN_MASK));
|
|
searchItem.addActionListener(e -> showSearchDialog());
|
|
|
|
JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard...");
|
|
selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK));
|
|
selectWildcardItem.addActionListener(e -> showWildcardSelectDialog());
|
|
|
|
JMenuItem refreshItem = new JMenuItem("Refresh");
|
|
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
|
|
refreshItem.addActionListener(e -> refreshPanels());
|
|
|
|
JMenuItem queueItem = new JMenuItem("Operations Queue...");
|
|
queueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.CTRL_DOWN_MASK));
|
|
queueItem.addActionListener(e -> OperationQueueDialog.showQueue(this));
|
|
|
|
JMenuItem exitItem = new JMenuItem("Exit");
|
|
exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0));
|
|
exitItem.addActionListener(e -> saveConfigAndExit());
|
|
|
|
fileMenu.add(searchItem);
|
|
fileMenu.add(selectWildcardItem);
|
|
fileMenu.add(refreshItem);
|
|
fileMenu.add(queueItem);
|
|
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");
|
|
settingsMenu.setMnemonic(KeyEvent.VK_O);
|
|
settingsMenu.addMouseListener(new MouseAdapter() {
|
|
@Override
|
|
public void mousePressed(MouseEvent e) {
|
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
|
showSettingsDialog();
|
|
}
|
|
}
|
|
});
|
|
// Add key binding for Alt+O since JMenu doesn't support accelerators directly
|
|
settingsMenu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
|
|
KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.ALT_DOWN_MASK), "openSettings");
|
|
settingsMenu.getActionMap().put("openSettings", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
showSettingsDialog();
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
requestFocusInActivePanel();
|
|
}
|
|
|
|
/**
|
|
* Apply appearance settings (font/colors) from config to UI components.
|
|
*/
|
|
private void applyAppearanceSettings() {
|
|
updateAutoRefreshTimer();
|
|
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);
|
|
|
|
// Update command line colors
|
|
if (commandLine != null) {
|
|
Component ed = commandLine.getEditor().getEditorComponent();
|
|
if (ed instanceof JTextField) {
|
|
JTextField tf = (JTextField) ed;
|
|
tf.setBackground(bg);
|
|
boolean dark = isDark(bg);
|
|
tf.setForeground(dark ? Color.WHITE : Color.BLACK);
|
|
Color selColor = config.getSelectionColor();
|
|
if (selColor != null) {
|
|
tf.setSelectionColor(selColor);
|
|
tf.setCaretColor(selColor);
|
|
} else {
|
|
tf.setCaretColor(dark ? Color.WHITE : Color.BLACK);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Color sel = config.getSelectionColor();
|
|
if (sel != null) {
|
|
if (leftPanel != null) leftPanel.applySelectionColor(sel);
|
|
if (rightPanel != null) rightPanel.applySelectionColor(sel);
|
|
|
|
// Apply selection color to command line editor for cursor and selection
|
|
if (commandLine != null) {
|
|
Component ed = commandLine.getEditor().getEditorComponent();
|
|
if (ed instanceof JTextField) {
|
|
JTextField tf = (JTextField) ed;
|
|
tf.setSelectionColor(sel);
|
|
tf.setCaretColor(sel);
|
|
tf.setSelectedTextColor(Color.WHITE); // Ensure selected text is readable on selection bg
|
|
}
|
|
}
|
|
|
|
// Ensure the active panel border uses the updated configuration color immediately
|
|
SwingUtilities.invokeLater(() -> updateActivePanelBorder());
|
|
}
|
|
|
|
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);
|
|
Color selColor = config != null ? config.getSelectionColor() : null;
|
|
|
|
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 javax.swing.text.JTextComponent) {
|
|
javax.swing.text.JTextComponent tc = (javax.swing.text.JTextComponent) c;
|
|
tc.setBackground(bg);
|
|
tc.setForeground(dark ? Color.WHITE : Color.BLACK);
|
|
if (selColor != null) {
|
|
tc.setSelectionColor(selColor);
|
|
tc.setCaretColor(selColor);
|
|
} else {
|
|
tc.setCaretColor(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 -> {
|
|
boolean textCleared = false;
|
|
Object currentItem = commandLine.getEditor().getItem();
|
|
if (currentItem != null && !currentItem.toString().isEmpty()) {
|
|
commandLine.getEditor().setItem("");
|
|
textCleared = true;
|
|
}
|
|
|
|
// If we just cleared text, or if focus is not in any table, return focus to active panel
|
|
Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
|
|
boolean inTable = (focusOwner instanceof JTable);
|
|
|
|
if (textCleared || !inTable) {
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
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+` - Home directory
|
|
rootPane.registerKeyboardAction(e -> {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().loadDirectory(new File(System.getProperty("user.home")));
|
|
}
|
|
}, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_QUOTE, InputEvent.CTRL_DOWN_MASK),
|
|
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
|
|
|
// Ctrl+C - Copy to clipboard
|
|
rootPane.registerKeyboardAction(e -> {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().copyToClipboard(false);
|
|
}
|
|
}, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK),
|
|
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
|
|
|
// Ctrl+X - Cut to clipboard
|
|
rootPane.registerKeyboardAction(e -> {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().copyToClipboard(true);
|
|
}
|
|
}, KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK),
|
|
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
|
|
|
// Ctrl+V - Paste from clipboard
|
|
rootPane.registerKeyboardAction(e -> {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().pasteFromClipboard();
|
|
}
|
|
}, KeyStroke.getKeyStroke(KeyEvent.VK_V, 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() {
|
|
Color selColor = config.getSelectionColor();
|
|
if (selColor == null) selColor = new Color(184, 207, 229);
|
|
|
|
// Delegate active state visualization to the panels themselves
|
|
leftPanel.setActive(activePanel == leftPanel, selColor);
|
|
rightPanel.setActive(activePanel == rightPanel, selColor);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
if (table == null) return;
|
|
|
|
// 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");
|
|
|
|
// Clipboard support (Ctrl+C, Ctrl+X, Ctrl+V)
|
|
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK), "clipboardCopy");
|
|
table.getActionMap().put("clipboardCopy", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().copyToClipboard(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), "clipboardCut");
|
|
table.getActionMap().put("clipboardCut", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().copyToClipboard(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), "clipboardPaste");
|
|
table.getActionMap().put("clipboardPaste", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().pasteFromClipboard();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Wildcard selection (Ctrl+A and +)
|
|
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK), "wildcardSelect");
|
|
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
.put(KeyStroke.getKeyStroke('+'), "wildcardSelect");
|
|
table.getActionMap().put("wildcardSelect", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
showWildcardSelectDialog();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
// Transfer to command line only for printable characters and when no modifiers like Ctrl or Alt are pressed.
|
|
// This prevents focus jump on shortcuts like Ctrl+C, Ctrl+V, etc.
|
|
// Exclude '+' as it is used for wildcard selection.
|
|
if (c != KeyEvent.CHAR_UNDEFINED && c >= 32 && c != 127 && c != '+' && !e.isControlDown() && !e.isAltDown()) {
|
|
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<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"No files selected",
|
|
"Copy",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
return;
|
|
}
|
|
|
|
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
|
File targetDir = targetPanel.getCurrentDirectory();
|
|
|
|
int result = showConfirmWithBackground(
|
|
String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
|
|
"Copy");
|
|
|
|
if (result == 0 || result == 1) {
|
|
boolean background = (result == 1);
|
|
if (background) {
|
|
addOperationToQueue("Copy", String.format("Copy %d items to %s", selectedItems.size(), targetDir.getName()),
|
|
(cb) -> FileOperations.copy(selectedItems, targetDir, cb), targetPanel);
|
|
} else {
|
|
performFileOperation((callback) -> {
|
|
FileOperations.copy(selectedItems, targetDir, callback);
|
|
}, "Copy completed", false, true, targetPanel);
|
|
}
|
|
} else {
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
activePanel.getFileTable().requestFocusInWindow();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move selected files to the opposite panel
|
|
*/
|
|
private void moveFiles() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"No files selected",
|
|
"Move",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
return;
|
|
}
|
|
|
|
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
|
File targetDir = targetPanel.getCurrentDirectory();
|
|
|
|
int result = showConfirmWithBackground(
|
|
String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
|
|
"Move");
|
|
|
|
if (result == 0 || result == 1) {
|
|
boolean background = (result == 1);
|
|
if (background) {
|
|
addOperationToQueue("Move", String.format("Move %d items to %s", selectedItems.size(), targetDir.getName()),
|
|
(cb) -> FileOperations.move(selectedItems, targetDir, cb), activePanel, targetPanel);
|
|
} else {
|
|
performFileOperation((callback) -> {
|
|
FileOperations.move(selectedItems, targetDir, callback);
|
|
}, "Move completed", false, true, activePanel, targetPanel);
|
|
}
|
|
} else {
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
activePanel.getFileTable().requestFocusInWindow();
|
|
}
|
|
}
|
|
}
|
|
|
|
private int showConfirmWithBackground(String message, String title) {
|
|
Object[] options = {"OK", "Background (F2)", "Cancel"};
|
|
JOptionPane pane = new JOptionPane(message, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[0]);
|
|
JDialog dialog = pane.createDialog(this, title);
|
|
|
|
pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "background");
|
|
pane.getActionMap().put("background", new AbstractAction() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
pane.setValue(options[1]);
|
|
dialog.dispose();
|
|
}
|
|
});
|
|
|
|
dialog.setVisible(true);
|
|
|
|
// Ensure focus returns to main window after dialog
|
|
this.toFront();
|
|
requestFocusInActivePanel();
|
|
|
|
Object selectedValue = pane.getValue();
|
|
if (selectedValue == null) return 2; // Cancel
|
|
if (selectedValue.equals(options[0])) return 0; // OK
|
|
if (selectedValue.equals(options[1])) return 1; // Background
|
|
return 2; // Cancel
|
|
}
|
|
|
|
/**
|
|
* Delete selected files
|
|
*/
|
|
private void deleteFiles() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"No files selected",
|
|
"Delete",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
return;
|
|
}
|
|
// remember current selection index so we can restore selection after deletion
|
|
final int rememberedIndex = (activePanel != null && activePanel.getCurrentTab() != null) ?
|
|
activePanel.getCurrentTab().getFocusedItemIndex() : -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 = showConfirmWithBackground(message.toString(), "Delete");
|
|
|
|
if (result == 0 || result == 1) {
|
|
boolean background = (result == 1);
|
|
if (background) {
|
|
addOperationToQueue("Delete", String.format("Delete %d items", selectedItems.size()),
|
|
(cb) -> FileOperations.delete(selectedItems, cb), activePanel);
|
|
} else {
|
|
performFileOperation((callback) -> {
|
|
FileOperations.delete(selectedItems, callback);
|
|
}, "Delete completed", false, true, activePanel);
|
|
|
|
// After deletion and refresh, restore selection: move focus to the nearest higher item.
|
|
SwingUtilities.invokeLater(() -> {
|
|
try {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
// Use another invokeLater to ensure BRIEF mode layout is updated
|
|
SwingUtilities.invokeLater(() -> {
|
|
activePanel.getCurrentTab().selectItemByIndex(rememberedIndex);
|
|
});
|
|
}
|
|
} catch (Exception ignore) {}
|
|
});
|
|
}
|
|
} else {
|
|
requestFocusInActivePanel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zip selected files
|
|
*/
|
|
private void zipFiles() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"No files selected",
|
|
"Zip",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
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;
|
|
int result = showConfirmWithBackground(
|
|
String.format("Zip %d items to:\n%s", selectedItems.size(), targetZip.getAbsolutePath()),
|
|
"Zip");
|
|
|
|
if (result == 0 || result == 1) {
|
|
boolean background = (result == 1);
|
|
if (background) {
|
|
addOperationToQueue("Zip", String.format("Zip %d items to %s", selectedItems.size(), finalTargetZip.getName()),
|
|
(cb) -> FileOperations.zip(selectedItems, finalTargetZip, cb), targetPanel);
|
|
} else {
|
|
performFileOperation((callback) -> {
|
|
FileOperations.zip(selectedItems, finalTargetZip, callback);
|
|
}, "Zipped into " + zipName, false, true, targetPanel);
|
|
}
|
|
} else {
|
|
requestFocusInActivePanel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unzip selected zip file
|
|
*/
|
|
private void unzipFiles() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"No files selected",
|
|
"Unzip",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
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);
|
|
requestFocusInActivePanel();
|
|
return;
|
|
}
|
|
|
|
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
|
File targetDir = targetPanel.getCurrentDirectory();
|
|
|
|
int result = showConfirmWithBackground(
|
|
String.format("Unzip %s to:\n%s", zipFile.getName(), targetDir.getAbsolutePath()),
|
|
"Unzip");
|
|
|
|
if (result == 0 || result == 1) {
|
|
boolean background = (result == 1);
|
|
if (background) {
|
|
addOperationToQueue("Unzip", String.format("Unzip %s to %s", zipFile.getName(), targetDir.getName()),
|
|
(cb) -> FileOperations.unzip(zipFile, targetDir, cb), targetPanel);
|
|
} else {
|
|
performFileOperation((callback) -> {
|
|
FileOperations.unzip(zipFile, targetDir, callback);
|
|
}, "Unzipped into " + targetDir.getName(), false, true, 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<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.size() != 1) {
|
|
JOptionPane.showMessageDialog(this,
|
|
"Select one file to rename",
|
|
"Rename",
|
|
JOptionPane.INFORMATION_MESSAGE);
|
|
requestFocusInActivePanel();
|
|
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 dirNameInput = JOptionPane.showInputDialog(this,
|
|
"New directory name:",
|
|
"New directory");
|
|
|
|
if (dirNameInput != null && !dirNameInput.trim().isEmpty()) {
|
|
final String dirName = dirNameInput.trim();
|
|
performFileOperation((callback) -> {
|
|
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName);
|
|
}, "Directory created", false, () -> {
|
|
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
|
activePanel.getCurrentTab().selectItem(dirName);
|
|
}
|
|
}, 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.addWindowListener(new java.awt.event.WindowAdapter() {
|
|
@Override
|
|
public void windowClosed(java.awt.event.WindowEvent e) {
|
|
requestFocusInActivePanel();
|
|
}
|
|
});
|
|
dialog.setVisible(true);
|
|
}
|
|
|
|
/**
|
|
* Show wildcard select dialog
|
|
*/
|
|
public void showWildcardSelectDialog() {
|
|
if (activePanel == null) return;
|
|
|
|
WildcardSelectDialog dialog = new WildcardSelectDialog(this);
|
|
dialog.setVisible(true);
|
|
|
|
String pattern = dialog.getPattern();
|
|
if (pattern != null && !pattern.isEmpty()) {
|
|
activePanel.selectByWildcard(pattern);
|
|
} else {
|
|
// If cancelled, return focus to the active panel
|
|
if (activePanel.getFileTable() != null) {
|
|
activePanel.getFileTable().requestFocusInWindow();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the given file's parent directory in the panel that currently has focus
|
|
* and select the file in that panel.
|
|
*/
|
|
public void showFileInFocusedPanel(File file) {
|
|
if (file == null) return;
|
|
// Determine which panel currently has focus
|
|
java.awt.Component owner = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
|
|
FilePanel target = null;
|
|
if (owner != null) {
|
|
Component p = owner;
|
|
while (p != null) {
|
|
if (p == leftPanel) {
|
|
target = leftPanel;
|
|
break;
|
|
}
|
|
if (p == rightPanel) {
|
|
target = rightPanel;
|
|
break;
|
|
}
|
|
p = p.getParent();
|
|
}
|
|
}
|
|
if (target == null) target = activePanel != null ? activePanel : leftPanel;
|
|
|
|
final FilePanel chosen = target;
|
|
final File parentDir = file.getParentFile();
|
|
if (parentDir == null) return;
|
|
|
|
// Load directory and then select item by name on the EDT
|
|
SwingUtilities.invokeLater(() -> {
|
|
chosen.loadDirectory(parentDir);
|
|
// mark this panel active and refresh borders
|
|
activePanel = chosen;
|
|
updateActivePanelBorder();
|
|
// After loading, select the file name
|
|
SwingUtilities.invokeLater(() -> {
|
|
FilePanelTab tab = chosen.getCurrentTab();
|
|
if (tab != null) {
|
|
tab.selectItem(file.getName());
|
|
tab.getFileTable().requestFocusInWindow();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show file in internal viewer
|
|
*/
|
|
private void viewFile() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
FileItem item = selectedItems.get(0);
|
|
if (item.isDirectory() || item.getName().equals("..")) {
|
|
return;
|
|
}
|
|
|
|
File file = item.getFile();
|
|
|
|
// Removed previous 10 MB limit: allow opening large files in the viewer (paged hex will stream large binaries).
|
|
|
|
FileEditor viewer = new FileEditor(this, file, config, true);
|
|
viewer.addWindowListener(new java.awt.event.WindowAdapter() {
|
|
@Override
|
|
public void windowClosed(java.awt.event.WindowEvent e) {
|
|
requestFocusInActivePanel();
|
|
}
|
|
});
|
|
viewer.setVisible(true);
|
|
}
|
|
|
|
/**
|
|
* Open file in internal editor
|
|
*/
|
|
private void editFile() {
|
|
List<FileItem> selectedItems = activePanel.getSelectedItems();
|
|
if (selectedItems.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
FileItem item = selectedItems.get(0);
|
|
if (item.isDirectory() || item.getName().equals("..")) {
|
|
return;
|
|
}
|
|
|
|
File file = item.getFile();
|
|
|
|
// If an external editor is configured, try launching it with the file path
|
|
String ext = config.getExternalEditorPath();
|
|
if (ext != null && !ext.trim().isEmpty()) {
|
|
try {
|
|
java.util.List<String> cmd = new java.util.ArrayList<>();
|
|
cmd.add(ext);
|
|
cmd.add(file.getAbsolutePath());
|
|
new ProcessBuilder(cmd).start();
|
|
return;
|
|
} catch (Exception ex) {
|
|
// Fall back to internal editor if external fails
|
|
JOptionPane.showMessageDialog(this,
|
|
"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.addWindowListener(new java.awt.event.WindowAdapter() {
|
|
@Override
|
|
public void windowClosed(java.awt.event.WindowEvent e) {
|
|
requestFocusInActivePanel();
|
|
}
|
|
});
|
|
editor.setVisible(true);
|
|
}
|
|
|
|
/**
|
|
* Refresh both panels while preserving selection and active panel focus.
|
|
*/
|
|
private void refreshPanels() {
|
|
if (leftPanel != null && leftPanel.getCurrentDirectory() != null) {
|
|
leftPanel.refresh(activePanel == leftPanel);
|
|
}
|
|
if (rightPanel != null && rightPanel.getCurrentDirectory() != null) {
|
|
rightPanel.refresh(activePanel == rightPanel);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<String> 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<String> parseCommand(String command) {
|
|
List<String> 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);
|
|
requestFocusInActivePanel();
|
|
}
|
|
|
|
/**
|
|
* Execute file operation with error handling
|
|
*/
|
|
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, FilePanel... panelsToRefresh) {
|
|
performFileOperation(operation, successMessage, showBytes, true, null, panelsToRefresh);
|
|
}
|
|
|
|
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, boolean modal, FilePanel... panelsToRefresh) {
|
|
performFileOperation(operation, successMessage, showBytes, modal, null, panelsToRefresh);
|
|
}
|
|
|
|
/**
|
|
* Execute file operation with error handling and a task to run after completion and refresh.
|
|
*/
|
|
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, Runnable postTask, FilePanel... panelsToRefresh) {
|
|
performFileOperation(operation, successMessage, showBytes, true, postTask, panelsToRefresh);
|
|
}
|
|
|
|
/**
|
|
* Execute file operation with error handling and a task to run after completion and refresh.
|
|
*/
|
|
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, boolean modal, Runnable postTask, FilePanel... panelsToRefresh) {
|
|
ProgressDialog progressDialog = new ProgressDialog(this, "File Operation", modal);
|
|
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();
|
|
}
|
|
|
|
@Override
|
|
public FileOperations.OverwriteResponse confirmOverwrite(File file) {
|
|
final FileOperations.OverwriteResponse[] result = new FileOperations.OverwriteResponse[1];
|
|
try {
|
|
SwingUtilities.invokeAndWait(() -> {
|
|
Object[] options = {"Yes", "Yes to All", "No", "No to All", "Cancel"};
|
|
int n = JOptionPane.showOptionDialog(progressDialog,
|
|
"File already exists: " + file.getName() + "\nOverwrite?",
|
|
"Overwrite Confirmation",
|
|
JOptionPane.DEFAULT_OPTION,
|
|
JOptionPane.QUESTION_MESSAGE,
|
|
null,
|
|
options,
|
|
options[0]);
|
|
|
|
switch (n) {
|
|
case 0: result[0] = FileOperations.OverwriteResponse.YES; break;
|
|
case 1: result[0] = FileOperations.OverwriteResponse.YES_TO_ALL; break;
|
|
case 2: result[0] = FileOperations.OverwriteResponse.NO; break;
|
|
case 3: result[0] = FileOperations.OverwriteResponse.NO_TO_ALL; break;
|
|
default:
|
|
result[0] = FileOperations.OverwriteResponse.CANCEL;
|
|
progressDialog.cancel();
|
|
break;
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
result[0] = FileOperations.OverwriteResponse.CANCEL;
|
|
}
|
|
return result[0];
|
|
}
|
|
|
|
@Override
|
|
public FileOperations.ErrorResponse onError(File file, Exception e) {
|
|
final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1];
|
|
try {
|
|
SwingUtilities.invokeAndWait(() -> {
|
|
Object[] options = {"Skip", "Retry", "Abort"};
|
|
int n = JOptionPane.showOptionDialog(progressDialog,
|
|
"Error operating on file: " + file.getName() + "\n" + e.getMessage(),
|
|
"Error",
|
|
JOptionPane.DEFAULT_OPTION,
|
|
JOptionPane.ERROR_MESSAGE,
|
|
null,
|
|
options,
|
|
options[0]);
|
|
|
|
switch (n) {
|
|
case 0: result[0] = FileOperations.ErrorResponse.SKIP; break;
|
|
case 1: result[0] = FileOperations.ErrorResponse.RETRY; break;
|
|
default:
|
|
result[0] = FileOperations.ErrorResponse.ABORT;
|
|
progressDialog.cancel();
|
|
break;
|
|
}
|
|
});
|
|
} catch (Exception ex) {
|
|
result[0] = FileOperations.ErrorResponse.ABORT;
|
|
}
|
|
return result[0];
|
|
}
|
|
};
|
|
|
|
// Run operation in a background thread
|
|
new Thread(() -> {
|
|
try {
|
|
operation.execute(callback);
|
|
|
|
SwingUtilities.invokeLater(() -> {
|
|
progressDialog.dispose();
|
|
|
|
// Force the window to front and request focus after modal dialog
|
|
MainWindow.this.toFront();
|
|
|
|
for (FilePanel panel : panelsToRefresh) {
|
|
if (panel.getCurrentDirectory() != null) {
|
|
panel.loadDirectory(panel.getCurrentDirectory(), false, false);
|
|
}
|
|
}
|
|
|
|
if (postTask != null) {
|
|
SwingUtilities.invokeLater(postTask);
|
|
}
|
|
|
|
requestFocusInActivePanel();
|
|
|
|
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() {
|
|
if (autoRefreshTimer != null) {
|
|
autoRefreshTimer.stop();
|
|
}
|
|
// Save window state
|
|
config.saveWindowState(this);
|
|
|
|
// Save active panel
|
|
if (activePanel == leftPanel) {
|
|
config.setActivePanel("left");
|
|
} else if (activePanel == rightPanel) {
|
|
config.setActivePanel("right");
|
|
}
|
|
|
|
// Save current panel paths (for backward compatibility)
|
|
if (leftPanel.getCurrentDirectory() != null) {
|
|
config.setLeftPanelPath(leftPanel.getCurrentDirectory().getAbsolutePath());
|
|
}
|
|
if (rightPanel.getCurrentDirectory() != null) {
|
|
config.setRightPanelPath(rightPanel.getCurrentDirectory().getAbsolutePath());
|
|
}
|
|
|
|
// Save open tabs + view modes and selected index for both panels
|
|
try {
|
|
java.util.List<String> leftPaths = leftPanel.getTabPaths();
|
|
java.util.List<String> leftModes = leftPanel.getTabViewModes();
|
|
java.util.List<String> leftFocused = leftPanel.getTabFocusedItems();
|
|
int leftSelected = leftPanel.getSelectedTabIndex();
|
|
config.saveLeftPanelTabs(leftPaths, leftModes, leftFocused, leftSelected);
|
|
} catch (Exception ex) {
|
|
// ignore
|
|
}
|
|
|
|
try {
|
|
java.util.List<String> rightPaths = rightPanel.getTabPaths();
|
|
java.util.List<String> rightModes = rightPanel.getTabViewModes();
|
|
java.util.List<String> rightFocused = rightPanel.getTabFocusedItems();
|
|
int rightSelected = rightPanel.getSelectedTabIndex();
|
|
config.saveRightPanelTabs(rightPaths, rightModes, rightFocused, rightSelected);
|
|
} catch (Exception ex) {
|
|
// ignore
|
|
}
|
|
|
|
// Save command history
|
|
java.util.List<String> 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;
|
|
}
|
|
|
|
private void addOperationToQueue(String title, String description, FileOperation operation, FilePanel... panelsToRefresh) {
|
|
FileOperationQueue.QueuedTask task = new FileOperationQueue.QueuedTask(title, description, (callback) -> {
|
|
operation.execute(callback);
|
|
|
|
SwingUtilities.invokeLater(() -> {
|
|
for (FilePanel panel : panelsToRefresh) {
|
|
if (panel.getCurrentDirectory() != null) {
|
|
panel.loadDirectory(panel.getCurrentDirectory(), false, false);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
FileOperationQueue.getInstance().addTask(task);
|
|
OperationQueueDialog.showQueue(this);
|
|
}
|
|
|
|
private void requestFocusInActivePanel() {
|
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
activePanel.getFileTable().requestFocusInWindow();
|
|
}
|
|
}
|
|
|
|
private void updateAutoRefreshTimer() {
|
|
if (autoRefreshTimer != null) {
|
|
autoRefreshTimer.stop();
|
|
}
|
|
int interval = config.getAutoRefreshInterval();
|
|
autoRefreshTimer = new Timer(interval, e -> {
|
|
if (activePanel != null && activePanel.getCurrentDirectory() != null) {
|
|
activePanel.refresh(false);
|
|
}
|
|
});
|
|
autoRefreshTimer.start();
|
|
}
|
|
}
|