Compare commits
2 Commits
896eafe62b
...
d116ed7d0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d116ed7d0f | ||
|
|
78cea4d18d |
@ -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 ---
|
||||
public String getGlobalFontName() {
|
||||
return properties.getProperty("global.font.name", "Monospaced");
|
||||
|
||||
@ -891,10 +891,87 @@ public class FilePanelTab extends JPanel {
|
||||
}
|
||||
} else if (item.isDirectory()) {
|
||||
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
|
||||
* mouse-driven selection is blocked). This mirrors the behavior of
|
||||
@ -931,6 +1008,8 @@ public class FilePanelTab extends JPanel {
|
||||
}
|
||||
} else if (item.isDirectory()) {
|
||||
loadDirectory(item.getFile());
|
||||
} else if (item.getFile().isFile()) {
|
||||
openFileNative(item.getFile());
|
||||
}
|
||||
}
|
||||
|
||||
@ -949,13 +1028,7 @@ public class FilePanelTab extends JPanel {
|
||||
if (item.isDirectory()) {
|
||||
loadDirectory(item.getFile());
|
||||
} else {
|
||||
try {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop.getDesktop().open(item.getFile());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot open: " + ex.getMessage()); } catch (Exception ignore) {}
|
||||
}
|
||||
openFileNative(item.getFile());
|
||||
}
|
||||
});
|
||||
menu.add(openItem);
|
||||
|
||||
@ -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<String, String> 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<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
|
||||
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("<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) {
|
||||
if (s == null || s.isEmpty()) return s;
|
||||
return s.substring(0,1).toUpperCase() + s.substring(1).toLowerCase();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user