Compare commits

..

2 Commits

Author SHA1 Message Date
Radek Davidek
d116ed7d0f fixes 2026-01-16 17:01:50 +01:00
Radek Davidek
78cea4d18d file associations 2026-01-16 17:01:47 +01:00
3 changed files with 174 additions and 7 deletions

View File

@ -244,6 +244,37 @@ public class AppConfig {
} }
} }
// --- File Associations ---
/** Returns a map of pattern (extension or wildcard) -> command */
public java.util.Map<String, String> getFileAssociations() {
java.util.Map<String, String> 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<String, String> 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<String, String> entry : associations.entrySet()) {
String pattern = entry.getKey().trim();
if (!pattern.isEmpty()) {
properties.setProperty("assoc." + pattern, entry.getValue());
}
}
}
}
// --- Appearance (global) settings --- // --- Appearance (global) settings ---
public String getGlobalFontName() { public String getGlobalFontName() {
return properties.getProperty("global.font.name", "Monospaced"); return properties.getProperty("global.font.name", "Monospaced");

View File

@ -891,10 +891,87 @@ public class FilePanelTab extends JPanel {
} }
} else if (item.isDirectory()) { } else if (item.isDirectory()) {
loadDirectory(item.getFile()); loadDirectory(item.getFile());
} else if (item.getFile().isFile()) {
openFileNative(item.getFile());
} }
} }
} }
private void openFileNative(File file) {
try {
// 1. Check custom associations from config (wildcard matching)
if (persistedConfig != null) {
java.util.Map<String, String> 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<String> 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<String> 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) {
try {
JOptionPane.showMessageDialog(this, "Error opening file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} catch (Exception ignore) {}
}
}
/** /**
* Open the item located at the given point (used for double-clicks while * Open the item located at the given point (used for double-clicks while
* mouse-driven selection is blocked). This mirrors the behavior of * mouse-driven selection is blocked). This mirrors the behavior of
@ -931,6 +1008,8 @@ public class FilePanelTab extends JPanel {
} }
} else if (item.isDirectory()) { } else if (item.isDirectory()) {
loadDirectory(item.getFile()); loadDirectory(item.getFile());
} else if (item.getFile().isFile()) {
openFileNative(item.getFile());
} }
} }
@ -949,13 +1028,7 @@ public class FilePanelTab extends JPanel {
if (item.isDirectory()) { if (item.isDirectory()) {
loadDirectory(item.getFile()); loadDirectory(item.getFile());
} else { } else {
try { openFileNative(item.getFile());
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().open(item.getFile());
}
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot open: " + ex.getMessage()); } catch (Exception ignore) {}
}
} }
}); });
menu.add(openItem); menu.add(openItem);

View File

@ -3,6 +3,7 @@ package com.kfmanager.ui;
import com.kfmanager.config.AppConfig; import com.kfmanager.config.AppConfig;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.HashMap;
@ -29,6 +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;
// Appearance controls // Appearance controls
private JButton appearanceFontBtn; private JButton appearanceFontBtn;
@ -58,6 +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());
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(700, 420); setSize(700, 420);
@ -69,6 +72,7 @@ public class SettingsDialog extends JDialog {
model.addElement("Editor"); model.addElement("Editor");
model.addElement("Sorting"); model.addElement("Sorting");
model.addElement("Toolbar"); model.addElement("Toolbar");
model.addElement("Associations");
categoryList = new JList<>(model); categoryList = new JList<>(model);
categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
categoryList.setSelectedIndex(0); categoryList.setSelectedIndex(0);
@ -81,6 +85,7 @@ public class SettingsDialog extends JDialog {
cards.add(buildEditorPanel(), "Editor"); cards.add(buildEditorPanel(), "Editor");
cards.add(buildSortingPanel(), "Sorting"); cards.add(buildSortingPanel(), "Sorting");
cards.add(buildToolbarPanel(), "Toolbar"); cards.add(buildToolbarPanel(), "Toolbar");
cards.add(buildAssociationsPanel(), "Associations");
categoryList.addListSelectionListener(e -> { categoryList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) { if (!e.getValueIsAdjusting()) {
@ -169,6 +174,25 @@ public class SettingsDialog extends JDialog {
if (is != null) config.setToolbarIconSize((Integer) is.getValue()); if (is != null) config.setToolbarIconSize((Integer) is.getValue());
} catch (Exception ignore) {} } 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<String, String> 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 // Save external editor path
if (externalEditorField != null) { if (externalEditorField != null) {
@ -192,6 +216,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);
// Notify UI to revert changes // Notify UI to revert changes
if (onChange != null) onChange.run(); if (onChange != null) onChange.run();
@ -517,6 +542,44 @@ public class SettingsDialog extends JDialog {
return p; 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("<html>Use <b>wildcards</b> (e.g. *.tar.gz) or plain extension (e.g. txt).<br>" +
"Use <b>%f</b> for full path, <b>%n</b> for filename. " +
"If no placeholder is used, path is appended to the end.</html>");
p.add(hint, BorderLayout.NORTH);
DefaultTableModel model = new DefaultTableModel(new String[]{"Pattern", "Command"}, 0);
java.util.Map<String, String> current = config.getFileAssociations();
for (java.util.Map.Entry<String, String> 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) { private static String capitalize(String s) {
if (s == null || s.isEmpty()) return s; if (s == null || s.isEmpty()) return s;
return s.substring(0,1).toUpperCase() + s.substring(1).toLowerCase(); return s.substring(0,1).toUpperCase() + s.substring(1).toLowerCase();