new system for associations

This commit is contained in:
rdavidek 2026-01-21 23:23:59 +01:00
parent 5aeba21e9a
commit a4afc103ad
3 changed files with 88 additions and 144 deletions

View File

@ -367,42 +367,57 @@ public class AppConfig {
} }
} }
// --- File Associations --- // --- Open With Configuration ---
/** Returns a map of pattern (extension or wildcard) -> command */ public static class OpenWithEntry {
public java.util.Map<String, String> getFileAssociations() { public String label;
java.util.Map<String, String> map = new java.util.HashMap<>(); public String type; // "directory" or extension
for (String key : properties.stringPropertyNames()) { public String command;
if (key.startsWith("assoc.")) {
String pattern = key.substring(6);
map.put(pattern, properties.getProperty(key));
}
}
return map;
}
public void setFileAssociations(java.util.Map<String, String> associations) { public OpenWithEntry() {}
// Remove all current associations first public OpenWithEntry(String label, String type, String command) {
for (String key : properties.stringPropertyNames()) { this.label = label;
if (key.startsWith("assoc.")) { this.type = type;
properties.remove(key); this.command = command;
}
}
// Add new ones
if (associations != null) {
for (java.util.Map.Entry<String, String> entry : associations.entrySet()) {
String pattern = entry.getKey().trim();
if (!pattern.isEmpty()) {
properties.setProperty("assoc." + pattern, entry.getValue());
}
}
} }
} }
/** Add or update a single file association */ public java.util.List<OpenWithEntry> getOpenWithEntries() {
public void setFileAssociation(String pattern, String command) { java.util.List<OpenWithEntry> list = new java.util.ArrayList<>();
if (pattern == null || pattern.trim().isEmpty()) return; String countStr = properties.getProperty("openwith.count", "0");
properties.setProperty("assoc." + pattern.trim(), command); int count = Integer.parseInt(countStr);
saveConfig(); for (int i = 0; i < count; i++) {
String label = properties.getProperty("openwith." + i + ".label");
String type = properties.getProperty("openwith." + i + ".type");
String command = properties.getProperty("openwith." + i + ".command");
if (label != null && type != null && command != null) {
list.add(new OpenWithEntry(label, type, command));
}
}
return list;
}
public void setOpenWithEntries(java.util.List<OpenWithEntry> entries) {
// Remove old entries
String countStr = properties.getProperty("openwith.count", "0");
int count = Integer.parseInt(countStr);
for (int i = 0; i < count; i++) {
properties.remove("openwith." + i + ".label");
properties.remove("openwith." + i + ".type");
properties.remove("openwith." + i + ".command");
}
if (entries == null) {
properties.setProperty("openwith.count", "0");
return;
}
properties.setProperty("openwith.count", String.valueOf(entries.size()));
for (int i = 0; i < entries.size(); i++) {
OpenWithEntry e = entries.get(i);
properties.setProperty("openwith." + i + ".label", e.label != null ? e.label : "");
properties.setProperty("openwith." + i + ".type", e.type != null ? e.type : "");
properties.setProperty("openwith." + i + ".command", e.command != null ? e.command : "");
}
} }
// --- Appearance (global) settings --- // --- Appearance (global) settings ---

View File

@ -1346,48 +1346,22 @@ public class FilePanelTab extends JPanel {
private void openFileNative(File file) { private void openFileNative(File file) {
try { try {
// 1. Check custom associations from config (wildcard matching) // 1. Check custom Open with entries from config
if (persistedConfig != null) { if (persistedConfig != null) {
java.util.Map<String, String> associations = persistedConfig.getFileAssociations(); java.util.List<cz.kamma.kfmanager.config.AppConfig.OpenWithEntry> owEntries = persistedConfig.getOpenWithEntries();
String name = file.getName(); String name = file.getName().toLowerCase();
String ext = "";
int dot = name.lastIndexOf('.');
if (dot > 0 && dot < name.length() - 1) {
ext = name.substring(dot + 1);
}
String command = null; String command = null;
for (cz.kamma.kfmanager.config.AppConfig.OpenWithEntry entry : owEntries) {
// Sort associations by length descending to match more specific patterns first (e.g. *.tar.gz before *.gz) if (entry.type != null && entry.type.equalsIgnoreCase(ext)) {
java.util.List<String> patterns = new java.util.ArrayList<>(associations.keySet()); command = entry.command;
patterns.sort((a, b) -> Integer.compare(b.length(), a.length())); break; // Use the first matching entry
for (String pattern : patterns) {
String fullCommand = associations.get(pattern);
// Support multiple patterns separated by semicolon (e.g. "jpg;png;gif")
String[] subPatterns = pattern.split(";");
boolean matched = false;
for (String sub : subPatterns) {
String glob = sub.trim();
if (glob.isEmpty()) continue;
// 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 = fullCommand;
matched = true;
break;
}
} catch (Exception ignore) {
// Fallback for simple extension match if glob is invalid
if (name.toLowerCase().endsWith("." + glob.toLowerCase())) {
command = fullCommand;
matched = true;
break;
}
}
} }
if (matched) break;
} }
if (command != null && !command.trim().isEmpty()) { if (command != null && !command.trim().isEmpty()) {
@ -1612,14 +1586,6 @@ public class FilePanelTab extends JPanel {
menu.addSeparator(); menu.addSeparator();
// Associate with...
JMenuItem associateItem = new JMenuItem("Associate with...");
associateItem.addActionListener(ae -> {
if (item.isDirectory() || item.getName().equals("..")) return;
showAssociationDialog(item.getFile());
});
menu.add(associateItem);
// Delete // Delete
JMenuItem deleteItem = new JMenuItem("Delete"); JMenuItem deleteItem = new JMenuItem("Delete");
deleteItem.addActionListener(ae -> { deleteItem.addActionListener(ae -> {
@ -3026,44 +2992,4 @@ public class FilePanelTab extends JPanel {
return null; return null;
} }
} }
private void showAssociationDialog(File file) {
String name = file.getName();
String initialPattern = "";
int lastDot = name.lastIndexOf('.');
if (lastDot > 0 && lastDot < name.length() - 1) {
initialPattern = "*." + name.substring(lastDot + 1).toLowerCase();
} else {
initialPattern = name; // fallback to full name if no extension
}
String pattern = JOptionPane.showInputDialog(this,
"File pattern (e.g. *.txt or txt):",
initialPattern);
if (pattern == null || pattern.trim().isEmpty()) return;
// If user just provided an extension like "txt", convert it to "*.txt" internally for consistency
String normalizedPattern = pattern.trim();
if (!normalizedPattern.contains("*") && !normalizedPattern.contains("?")) {
normalizedPattern = "*." + normalizedPattern;
}
String existingCmd = "";
if (persistedConfig != null) {
java.util.Map<String, String> associations = persistedConfig.getFileAssociations();
existingCmd = associations.getOrDefault(normalizedPattern, "");
}
String command = JOptionPane.showInputDialog(this,
"Command to run (%f for full path, %n for name):",
existingCmd);
if (command != null && !command.trim().isEmpty()) {
if (persistedConfig != null) {
persistedConfig.setFileAssociation(normalizedPattern, command.trim());
JOptionPane.showMessageDialog(this, "Association saved for " + normalizedPattern);
}
}
}
} }

View File

@ -30,7 +30,7 @@ public class SettingsDialog extends JDialog {
private final String originalExternalEditorPath; private final String originalExternalEditorPath;
private final int originalToolbarButtonSize; private final int originalToolbarButtonSize;
private final int originalToolbarIconSize; private final int originalToolbarIconSize;
private final java.util.Map<String, String> originalFileAssociations; private final java.util.List<cz.kamma.kfmanager.config.AppConfig.OpenWithEntry> originalOpenWithEntries;
// Appearance controls // Appearance controls
private JButton appearanceFontBtn; private JButton appearanceFontBtn;
@ -60,7 +60,7 @@ public class SettingsDialog extends JDialog {
this.originalExternalEditorPath = config.getExternalEditorPath(); this.originalExternalEditorPath = config.getExternalEditorPath();
this.originalToolbarButtonSize = config.getToolbarButtonSize(); this.originalToolbarButtonSize = config.getToolbarButtonSize();
this.originalToolbarIconSize = config.getToolbarIconSize(); this.originalToolbarIconSize = config.getToolbarIconSize();
this.originalFileAssociations = new java.util.HashMap<>(config.getFileAssociations()); this.originalOpenWithEntries = new java.util.ArrayList<>(config.getOpenWithEntries());
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(700, 420); setSize(700, 420);
@ -73,7 +73,7 @@ public class SettingsDialog extends JDialog {
model.addElement("Behavior"); model.addElement("Behavior");
model.addElement("Sorting"); model.addElement("Sorting");
model.addElement("Toolbar"); model.addElement("Toolbar");
model.addElement("Associations"); model.addElement("Open with");
model.addElement("Import/Export"); model.addElement("Import/Export");
categoryList = new JList<>(model); categoryList = new JList<>(model);
categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@ -88,7 +88,7 @@ public class SettingsDialog extends JDialog {
cards.add(buildBehaviorPanel(), "Behavior"); cards.add(buildBehaviorPanel(), "Behavior");
cards.add(buildSortingPanel(), "Sorting"); cards.add(buildSortingPanel(), "Sorting");
cards.add(buildToolbarPanel(), "Toolbar"); cards.add(buildToolbarPanel(), "Toolbar");
cards.add(buildAssociationsPanel(), "Associations"); cards.add(buildOpenWithPanel(), "Open with");
cards.add(buildImportExportPanel(), "Import/Export"); cards.add(buildImportExportPanel(), "Import/Export");
categoryList.addListSelectionListener(e -> { categoryList.addListSelectionListener(e -> {
@ -181,21 +181,24 @@ public class SettingsDialog extends JDialog {
} catch (Exception ignore) {} } catch (Exception ignore) {}
} }
// Collect Associations settings // Collect Open with settings
JPanel assocHolder = (JPanel) panels.get("Associations"); JPanel owHolder = (JPanel) panels.get("Open with");
if (assocHolder != null) { if (owHolder != null) {
try { try {
DefaultTableModel assocModel = (DefaultTableModel) assocHolder.getClientProperty("tableModel"); DefaultTableModel owModel = (DefaultTableModel) owHolder.getClientProperty("tableModel");
if (assocModel != null) { if (owModel != null) {
java.util.Map<String, String> map = new java.util.HashMap<>(); java.util.List<cz.kamma.kfmanager.config.AppConfig.OpenWithEntry> list = new java.util.ArrayList<>();
for (int i = 0; i < assocModel.getRowCount(); i++) { for (int i = 0; i < owModel.getRowCount(); i++) {
String ext = (String) assocModel.getValueAt(i, 0); String label = (String) owModel.getValueAt(i, 0);
String cmd = (String) assocModel.getValueAt(i, 1); String type = (String) owModel.getValueAt(i, 1);
if (ext != null && !ext.trim().isEmpty() && cmd != null && !cmd.trim().isEmpty()) { String cmd = (String) owModel.getValueAt(i, 2);
map.put(ext.trim(), cmd.trim()); if (label != null && !label.trim().isEmpty() &&
type != null && !type.trim().isEmpty() &&
cmd != null && !cmd.trim().isEmpty()) {
list.add(new cz.kamma.kfmanager.config.AppConfig.OpenWithEntry(label.trim(), type.trim(), cmd.trim()));
} }
} }
config.setFileAssociations(map); config.setOpenWithEntries(list);
} }
} catch (Exception ignore) {} } catch (Exception ignore) {}
} }
@ -231,7 +234,7 @@ public class SettingsDialog extends JDialog {
config.setExternalEditorPath(originalExternalEditorPath); config.setExternalEditorPath(originalExternalEditorPath);
config.setToolbarButtonSize(originalToolbarButtonSize); config.setToolbarButtonSize(originalToolbarButtonSize);
config.setToolbarIconSize(originalToolbarIconSize); config.setToolbarIconSize(originalToolbarIconSize);
config.setFileAssociations(originalFileAssociations); config.setOpenWithEntries(originalOpenWithEntries);
// Notify UI to revert changes // Notify UI to revert changes
if (onChange != null) onChange.run(); if (onChange != null) onChange.run();
@ -632,19 +635,19 @@ public class SettingsDialog extends JDialog {
return p; return p;
} }
private JPanel buildAssociationsPanel() { private JPanel buildOpenWithPanel() {
JPanel p = new JPanel(new BorderLayout(8, 8)); JPanel p = new JPanel(new BorderLayout(8, 8));
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
JLabel hint = new JLabel("<html>Use <b>wildcards</b> (e.g. *.tar.gz) or plain extension (e.g. txt).<br>" + JLabel hint = new JLabel("<html>Use <b>directory</b> or <b>extension</b> (e.g. txt) for Type.<br>" +
"Use <b>%f</b> for full path, <b>%n</b> for filename. " + "Use <b>%f</b> for full path, <b>%n</b> for filename. " +
"If no placeholder is used, path is appended to the end.</html>"); "If no placeholder is used, path is appended to the end.</html>");
p.add(hint, BorderLayout.NORTH); p.add(hint, BorderLayout.NORTH);
DefaultTableModel model = new DefaultTableModel(new String[]{"Pattern", "Command"}, 0); DefaultTableModel model = new DefaultTableModel(new String[]{"Label", "Type", "Command"}, 0);
java.util.Map<String, String> current = config.getFileAssociations(); java.util.List<cz.kamma.kfmanager.config.AppConfig.OpenWithEntry> current = config.getOpenWithEntries();
for (java.util.Map.Entry<String, String> entry : current.entrySet()) { for (cz.kamma.kfmanager.config.AppConfig.OpenWithEntry entry : current) {
model.addRow(new Object[]{entry.getKey(), entry.getValue()}); model.addRow(new Object[]{entry.label, entry.type, entry.command});
} }
JTable table = new JTable(model); JTable table = new JTable(model);
@ -652,7 +655,7 @@ public class SettingsDialog extends JDialog {
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addBtn = new JButton("Add"); JButton addBtn = new JButton("Add");
addBtn.addActionListener(e -> model.addRow(new Object[]{"", ""})); addBtn.addActionListener(e -> model.addRow(new Object[]{"", "", ""}));
JButton removeBtn = new JButton("Remove"); JButton removeBtn = new JButton("Remove");
removeBtn.addActionListener(e -> { removeBtn.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
@ -665,7 +668,7 @@ public class SettingsDialog extends JDialog {
// Save reference for OK action // Save reference for OK action
JPanel holder = new JPanel(); JPanel holder = new JPanel();
holder.putClientProperty("tableModel", model); holder.putClientProperty("tableModel", model);
panels.put("Associations", holder); panels.put("Open with", holder);
return p; return p;
} }