From d116ed7d0f3ef9112822970763ed78f901f545d6 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Fri, 16 Jan 2026 17:01:50 +0100 Subject: [PATCH] fixes --- .../java/com/kfmanager/config/AppConfig.java | 31 +++++++++ .../java/com/kfmanager/ui/FilePanelTab.java | 59 +++++++++++++++++ .../java/com/kfmanager/ui/SettingsDialog.java | 63 +++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/src/main/java/com/kfmanager/config/AppConfig.java b/src/main/java/com/kfmanager/config/AppConfig.java index 1e9d42d..e4fe840 100644 --- a/src/main/java/com/kfmanager/config/AppConfig.java +++ b/src/main/java/com/kfmanager/config/AppConfig.java @@ -244,6 +244,37 @@ public class AppConfig { } } + // --- File Associations --- + /** Returns a map of pattern (extension or wildcard) -> command */ + public java.util.Map getFileAssociations() { + java.util.Map map = new java.util.HashMap<>(); + for (String key : properties.stringPropertyNames()) { + if (key.startsWith("assoc.")) { + String pattern = key.substring(6); + map.put(pattern, properties.getProperty(key)); + } + } + return map; + } + + public void setFileAssociations(java.util.Map associations) { + // Remove all current associations first + for (String key : properties.stringPropertyNames()) { + if (key.startsWith("assoc.")) { + properties.remove(key); + } + } + // Add new ones + if (associations != null) { + for (java.util.Map.Entry entry : associations.entrySet()) { + String pattern = entry.getKey().trim(); + if (!pattern.isEmpty()) { + properties.setProperty("assoc." + pattern, entry.getValue()); + } + } + } + } + // --- Appearance (global) settings --- public String getGlobalFontName() { return properties.getProperty("global.font.name", "Monospaced"); diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index cc88a47..e1840b1 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -899,11 +899,70 @@ public class FilePanelTab extends JPanel { private void openFileNative(File file) { try { + // 1. Check custom associations from config (wildcard matching) + if (persistedConfig != null) { + java.util.Map associations = persistedConfig.getFileAssociations(); + String name = file.getName(); + String command = null; + + // Sort associations by length descending to match more specific patterns first (e.g. *.tar.gz before *.gz) + java.util.List patterns = new java.util.ArrayList<>(associations.keySet()); + patterns.sort((a, b) -> Integer.compare(b.length(), a.length())); + + for (String pattern : patterns) { + String glob = pattern; + // If user just provided an extension like "txt", convert to "*.txt" + if (!glob.contains("*") && !glob.contains("?")) { + glob = "*." + glob; + } + + try { + java.nio.file.PathMatcher matcher = java.nio.file.FileSystems.getDefault().getPathMatcher("glob:" + glob); + if (matcher.matches(java.nio.file.Paths.get(name))) { + command = associations.get(pattern); + break; + } + } catch (Exception ignore) { + // Fallback for simple extension match if glob is invalid + if (name.toLowerCase().endsWith("." + pattern.toLowerCase())) { + command = associations.get(pattern); + break; + } + } + } + + if (command != null && !command.trim().isEmpty()) { + String fullPath = file.getAbsolutePath(); + String fileName = file.getName(); + String trimmedCmd = command.trim(); + boolean hasPlaceholder = trimmedCmd.contains("%f") || trimmedCmd.contains("%n"); + + java.util.List cmdList = new java.util.ArrayList<>(); + String[] parts = trimmedCmd.split("\\s+"); + for (String part : parts) { + String processed = part.replace("%f", fullPath).replace("%n", fileName); + if (processed.startsWith("\"") && processed.endsWith("\"") && processed.length() >= 2) { + processed = processed.substring(1, processed.length() - 1); + } + cmdList.add(processed); + } + + if (!hasPlaceholder) { + cmdList.add(fullPath); + } + + new ProcessBuilder(cmdList).directory(file.getParentFile()).start(); + return; + } + } + + // 2. If executable, start it directly if (file.canExecute() && !file.isDirectory()) { new ProcessBuilder(file.getAbsolutePath()) .directory(file.getParentFile()) .start(); } else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + // 3. Fallback to system default Desktop.getDesktop().open(file); } } catch (Exception ex) { diff --git a/src/main/java/com/kfmanager/ui/SettingsDialog.java b/src/main/java/com/kfmanager/ui/SettingsDialog.java index c7f6c15..23680a8 100644 --- a/src/main/java/com/kfmanager/ui/SettingsDialog.java +++ b/src/main/java/com/kfmanager/ui/SettingsDialog.java @@ -3,6 +3,7 @@ package com.kfmanager.ui; import com.kfmanager.config.AppConfig; import javax.swing.*; +import javax.swing.table.DefaultTableModel; import java.awt.*; import java.io.File; import java.util.HashMap; @@ -29,6 +30,7 @@ public class SettingsDialog extends JDialog { private final String originalExternalEditorPath; private final int originalToolbarButtonSize; private final int originalToolbarIconSize; + private final java.util.Map originalFileAssociations; // Appearance controls private JButton appearanceFontBtn; @@ -58,6 +60,7 @@ public class SettingsDialog extends JDialog { this.originalExternalEditorPath = config.getExternalEditorPath(); this.originalToolbarButtonSize = config.getToolbarButtonSize(); this.originalToolbarIconSize = config.getToolbarIconSize(); + this.originalFileAssociations = new java.util.HashMap<>(config.getFileAssociations()); setDefaultCloseOperation(DISPOSE_ON_CLOSE); setSize(700, 420); @@ -69,6 +72,7 @@ public class SettingsDialog extends JDialog { model.addElement("Editor"); model.addElement("Sorting"); model.addElement("Toolbar"); + model.addElement("Associations"); categoryList = new JList<>(model); categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); categoryList.setSelectedIndex(0); @@ -81,6 +85,7 @@ public class SettingsDialog extends JDialog { cards.add(buildEditorPanel(), "Editor"); cards.add(buildSortingPanel(), "Sorting"); cards.add(buildToolbarPanel(), "Toolbar"); + cards.add(buildAssociationsPanel(), "Associations"); categoryList.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { @@ -169,6 +174,25 @@ public class SettingsDialog extends JDialog { if (is != null) config.setToolbarIconSize((Integer) is.getValue()); } catch (Exception ignore) {} } + + // Collect Associations settings + JPanel assocHolder = (JPanel) panels.get("Associations"); + if (assocHolder != null) { + try { + DefaultTableModel assocModel = (DefaultTableModel) assocHolder.getClientProperty("tableModel"); + if (assocModel != null) { + java.util.Map map = new java.util.HashMap<>(); + for (int i = 0; i < assocModel.getRowCount(); i++) { + String ext = (String) assocModel.getValueAt(i, 0); + String cmd = (String) assocModel.getValueAt(i, 1); + if (ext != null && !ext.trim().isEmpty() && cmd != null && !cmd.trim().isEmpty()) { + map.put(ext.trim(), cmd.trim()); + } + } + config.setFileAssociations(map); + } + } catch (Exception ignore) {} + } // Save external editor path if (externalEditorField != null) { @@ -192,6 +216,7 @@ public class SettingsDialog extends JDialog { config.setExternalEditorPath(originalExternalEditorPath); config.setToolbarButtonSize(originalToolbarButtonSize); config.setToolbarIconSize(originalToolbarIconSize); + config.setFileAssociations(originalFileAssociations); // Notify UI to revert changes if (onChange != null) onChange.run(); @@ -517,6 +542,44 @@ public class SettingsDialog extends JDialog { return p; } + private JPanel buildAssociationsPanel() { + JPanel p = new JPanel(new BorderLayout(8, 8)); + p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + + JLabel hint = new JLabel("Use wildcards (e.g. *.tar.gz) or plain extension (e.g. txt).
" + + "Use %f for full path, %n for filename. " + + "If no placeholder is used, path is appended to the end."); + p.add(hint, BorderLayout.NORTH); + + DefaultTableModel model = new DefaultTableModel(new String[]{"Pattern", "Command"}, 0); + java.util.Map current = config.getFileAssociations(); + for (java.util.Map.Entry entry : current.entrySet()) { + model.addRow(new Object[]{entry.getKey(), entry.getValue()}); + } + + JTable table = new JTable(model); + p.add(new JScrollPane(table), BorderLayout.CENTER); + + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton addBtn = new JButton("Add"); + addBtn.addActionListener(e -> model.addRow(new Object[]{"", ""})); + JButton removeBtn = new JButton("Remove"); + removeBtn.addActionListener(e -> { + int row = table.getSelectedRow(); + if (row >= 0) model.removeRow(row); + }); + btnPanel.add(addBtn); + btnPanel.add(removeBtn); + p.add(btnPanel, BorderLayout.SOUTH); + + // Save reference for OK action + JPanel holder = new JPanel(); + holder.putClientProperty("tableModel", model); + panels.put("Associations", holder); + + return p; + } + private static String capitalize(String s) { if (s == null || s.isEmpty()) return s; return s.substring(0,1).toUpperCase() + s.substring(1).toLowerCase();