diff --git a/src/main/java/com/kfmanager/config/AppConfig.java b/src/main/java/com/kfmanager/config/AppConfig.java index b52c5f7..0d75247 100644 --- a/src/main/java/com/kfmanager/config/AppConfig.java +++ b/src/main/java/com/kfmanager/config/AppConfig.java @@ -561,4 +561,54 @@ public class AppConfig { properties.remove("cmd.history." + i); } } + + // --- Toolbar shortcuts persistence --- + public static class ToolbarShortcut implements Serializable { + public String command; + public String label; + public String iconPath; + public String workingDir; + + public ToolbarShortcut(String command, String label, String iconPath, String workingDir) { + this.command = command; + this.label = label; + this.iconPath = iconPath; + this.workingDir = workingDir; + } + } + + public java.util.List getToolbarShortcuts() { + java.util.List list = new java.util.ArrayList<>(); + int count = Integer.parseInt(properties.getProperty("toolbar.shortcuts.count", "0")); + for (int i = 0; i < count; i++) { + String cmd = properties.getProperty("toolbar.shortcut." + i + ".command"); + String label = properties.getProperty("toolbar.shortcut." + i + ".label"); + String icon = properties.getProperty("toolbar.shortcut." + i + ".iconPath"); + String workDir = properties.getProperty("toolbar.shortcut." + i + ".workingDir"); + if (cmd != null) { + list.add(new ToolbarShortcut(cmd, label, icon, workDir)); + } + } + return list; + } + + public void saveToolbarShortcuts(java.util.List shortcuts) { + // remove old entries first + int old = Integer.parseInt(properties.getProperty("toolbar.shortcuts.count", "0")); + for (int i = 0; i < old; i++) { + properties.remove("toolbar.shortcut." + i + ".command"); + properties.remove("toolbar.shortcut." + i + ".label"); + properties.remove("toolbar.shortcut." + i + ".iconPath"); + properties.remove("toolbar.shortcut." + i + ".workingDir"); + } + + properties.setProperty("toolbar.shortcuts.count", String.valueOf(shortcuts.size())); + for (int i = 0; i < shortcuts.size(); i++) { + ToolbarShortcut s = shortcuts.get(i); + properties.setProperty("toolbar.shortcut." + i + ".command", s.command != null ? s.command : ""); + properties.setProperty("toolbar.shortcut." + i + ".label", s.label != null ? s.label : ""); + properties.setProperty("toolbar.shortcut." + i + ".iconPath", s.iconPath != null ? s.iconPath : ""); + properties.setProperty("toolbar.shortcut." + i + ".workingDir", s.workingDir != null ? s.workingDir : ""); + } + } } diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index 11ff245..94e3cd5 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -6,7 +6,10 @@ import javax.swing.*; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import java.awt.*; +import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -275,10 +278,32 @@ public class FilePanelTab extends JPanel { return; } - // Ignore mouse pressed/released/dragged events to prevent selection changes - if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED || - e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED || - e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) { + // Allow MOUSE_PRESSED for drag initiating gestures, but block standard selection change. + // We'll process selection manually in MOUSE_CLICKED above. + if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED) { + // Start selection logic on press to support DnD initiate + int col = columnAtPoint(e.getPoint()); + int row = rowAtPoint(e.getPoint()); + if (row >= 0) { + if (viewMode == ViewMode.BRIEF) { + FileItem item = tableModel.getItemFromBriefLayout(row, col); + if (item != null) { + int index = tableModel.items.indexOf(item); + if (index >= 0) { + int selRow = index % tableModel.briefRowsPerColumn; + fileTable.setRowSelectionInterval(selRow, selRow); + briefCurrentColumn = index / tableModel.briefRowsPerColumn; + } + } + } else { + fileTable.setRowSelectionInterval(row, row); + } + fileTable.requestFocusInWindow(); + repaint(); + } + } + + if (e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED) { e.consume(); return; } @@ -287,20 +312,60 @@ public class FilePanelTab extends JPanel { } @Override protected void processMouseMotionEvent(java.awt.event.MouseEvent e) { - // Block mouse-dragged events so dragging cannot change selection - // or initiate any drag behavior. Consume the event and do nothing. - if (e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) { - e.consume(); - return; - } + // Allow mouse movement to pass through to support Drag and Drop super.processMouseMotionEvent(e); } }; - // Disable Swing drag-and-drop support and transfer handling on the table - // to ensure no drag gestures or DnD occur. - fileTable.setDragEnabled(false); - fileTable.setTransferHandler(null); + // Enable Drag and Drop on the table + fileTable.setDragEnabled(true); + fileTable.setTransferHandler(new TransferHandler() { + @Override + public int getSourceActions(JComponent c) { + return COPY; + } + + @Override + protected Transferable createTransferable(JComponent c) { + List selected = getSelectedItems(); + if (selected.isEmpty()) { + // If nothing explicitly marked, use the focused item + FileItem focused = getFocusedItem(); + if (focused != null && !focused.getName().equals("..")) { + selected = new ArrayList<>(); + selected.add(focused); + } + } + + final List files = new ArrayList<>(); + for (FileItem item : selected) { + if (!item.getName().equals("..")) { + files.add(item.getFile()); + } + } + + if (files.isEmpty()) return null; + + return new Transferable() { + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[]{DataFlavor.javaFileListFlavor}; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return DataFlavor.javaFileListFlavor.equals(flavor); + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor); + return files; + } + }; + } + }); + fileTable.setFocusTraversalKeysEnabled(false); try { fileTable.setDropMode(null); diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java index aa7598e..0816b9a 100644 --- a/src/main/java/com/kfmanager/ui/MainWindow.java +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -7,8 +7,11 @@ 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; /** @@ -20,6 +23,7 @@ public class MainWindow extends JFrame { private FilePanel rightPanel; private FilePanel activePanel; private JPanel buttonPanel; + private JToolBar toolBar; private JComboBox commandLine; private AppConfig config; @@ -270,9 +274,37 @@ public class MainWindow extends JFrame { * Create toolbar with buttons for changing view mode */ private void createToolBar() { - JToolBar toolBar = new JToolBar(); - toolBar.setFloatable(false); - toolBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + 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"); @@ -299,12 +331,165 @@ public class MainWindow extends JFrame { toolBar.addSeparator(); - // Info label - JLabel infoLabel = new JLabel(" View Mode "); - infoLabel.setFont(infoLabel.getFont().deriveFont(Font.PLAIN, 10f)); - toolBar.add(infoLabel); + // Load custom shortcuts from config + List shortcuts = config.getToolbarShortcuts(); + for (AppConfig.ToolbarShortcut s : shortcuts) { + JButton btn = new JButton(); + 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(16, 16, 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); + } - add(toolBar, BorderLayout.NORTH); + 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(); } /** @@ -1361,6 +1546,82 @@ public class MainWindow extends JFrame { } } + 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;