kf-manager/src/main/java/com/kfmanager/ui/SettingsDialog.java
2026-01-16 16:32:16 +01:00

530 lines
23 KiB
Java

package com.kfmanager.ui;
import com.kfmanager.config.AppConfig;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Settings dialog with categories on the left and parameters on the right.
*/
public class SettingsDialog extends JDialog {
private final AppConfig config;
private final Runnable onChange;
private final JList<String> categoryList;
private final JPanel cards;
private final CardLayout cardLayout;
// Original values for cancellation
private final Color originalBg;
private final Color originalSel;
private final Color originalMark;
private final Color originalFolder;
private final Font originalGlobalFont;
private final Font originalEditorFont;
private final String originalExternalEditorPath;
private final int originalToolbarButtonSize;
private final int originalToolbarIconSize;
// Appearance controls
private JButton appearanceFontBtn;
private JButton appearanceBgBtn;
private JButton appearanceSelBtn;
private JButton appearanceMarkBtn;
private JButton appearanceFolderBtn;
// Editor controls
private JButton editorFontBtn;
private JTextField externalEditorField;
private final Map<String, JPanel> panels = new HashMap<>();
public SettingsDialog(Window parent, AppConfig config, Runnable onChange) {
super(parent, "Settings", ModalityType.APPLICATION_MODAL);
this.config = config;
this.onChange = onChange;
// Store original values
this.originalBg = config.getBackgroundColor();
this.originalSel = config.getSelectionColor();
this.originalMark = config.getMarkedColor();
this.originalFolder = config.getFolderColor();
this.originalGlobalFont = config.getGlobalFont();
this.originalEditorFont = config.getEditorFont();
this.originalExternalEditorPath = config.getExternalEditorPath();
this.originalToolbarButtonSize = config.getToolbarButtonSize();
this.originalToolbarIconSize = config.getToolbarIconSize();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(700, 420);
setLocationRelativeTo(parent);
// Left: categories
DefaultListModel<String> model = new DefaultListModel<>();
model.addElement("Appearance");
model.addElement("Editor");
model.addElement("Sorting");
model.addElement("Toolbar");
categoryList = new JList<>(model);
categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
categoryList.setSelectedIndex(0);
cardLayout = new CardLayout();
cards = new JPanel(cardLayout);
// Build category panels
cards.add(buildAppearancePanel(), "Appearance");
cards.add(buildEditorPanel(), "Editor");
cards.add(buildSortingPanel(), "Sorting");
cards.add(buildToolbarPanel(), "Toolbar");
categoryList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
String sel = categoryList.getSelectedValue();
if (sel != null) cardLayout.show(cards, sel);
}
});
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
new JScrollPane(categoryList), cards);
split.setResizeWeight(0);
split.setDividerLocation(160);
add(split, BorderLayout.CENTER);
JPanel btns = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton ok = new JButton("OK");
ok.addActionListener(e -> {
// Collect sorting settings from the Sorting panel (if present)
JPanel sortingHolder = (JPanel) panels.get("Sorting");
if (sortingHolder != null) {
try {
JComboBox<?> pf = (JComboBox<?>) sortingHolder.getClientProperty("primaryField");
JComboBox<?> po = (JComboBox<?>) sortingHolder.getClientProperty("primaryOrder");
JComboBox<?> sf = (JComboBox<?>) sortingHolder.getClientProperty("secondaryField");
JComboBox<?> so = (JComboBox<?>) sortingHolder.getClientProperty("secondaryOrder");
JComboBox<?> tf = (JComboBox<?>) sortingHolder.getClientProperty("tertiaryField");
JComboBox<?> to = (JComboBox<?>) sortingHolder.getClientProperty("tertiaryOrder");
java.util.List<String> criteria = new java.util.ArrayList<>();
if (pf != null && po != null) {
String f = pf.getSelectedItem() != null ? pf.getSelectedItem().toString().toLowerCase() : "";
String ord = po.getSelectedItem() != null && po.getSelectedItem().toString().equalsIgnoreCase("Descending") ? "desc" : "asc";
if (!f.isEmpty()) criteria.add(f + ":" + ord);
}
if (sf != null && so != null) {
String s = sf.getSelectedItem() != null ? sf.getSelectedItem().toString() : "(none)";
if (!"(none)".equals(s)) {
String field = s.toLowerCase();
String ord = so.getSelectedItem() != null && so.getSelectedItem().toString().equalsIgnoreCase("Descending") ? "desc" : "asc";
criteria.add(field + ":" + ord);
}
}
if (tf != null && to != null) {
String t = tf.getSelectedItem() != null ? tf.getSelectedItem().toString() : "(none)";
if (!"(none)".equals(t)) {
String field = t.toLowerCase();
String ord = to.getSelectedItem() != null && to.getSelectedItem().toString().equalsIgnoreCase("Descending") ? "desc" : "asc";
criteria.add(field + ":" + ord);
}
}
config.setMultipleSortCriteria(criteria);
// save extra sorting options
JComboBox<?> hiddenOrder = (JComboBox<?>) sortingHolder.getClientProperty("hiddenOrder");
JCheckBox uppercasePriority = (JCheckBox) sortingHolder.getClientProperty("uppercasePriority");
JCheckBox numericAware = (JCheckBox) sortingHolder.getClientProperty("numericAware");
if (hiddenOrder != null) {
boolean hiddenLast = "Hidden last".equals(hiddenOrder.getSelectedItem());
config.setHiddenFilesLast(hiddenLast);
}
if (uppercasePriority != null) {
config.setUppercasePriority(uppercasePriority.isSelected());
}
if (numericAware != null) {
config.setNumericSortEnabled(numericAware.isSelected());
}
JCheckBox ignoreLeadingDot = (JCheckBox) sortingHolder.getClientProperty("ignoreLeadingDot");
if (ignoreLeadingDot != null) {
config.setIgnoreLeadingDot(ignoreLeadingDot.isSelected());
}
} catch (Exception ignore) {}
}
// Collect Toolbar settings
JPanel toolbarHolder = (JPanel) panels.get("Toolbar");
if (toolbarHolder != null) {
try {
JSpinner bs = (JSpinner) toolbarHolder.getClientProperty("buttonSize");
JSpinner is = (JSpinner) toolbarHolder.getClientProperty("iconSize");
if (bs != null) config.setToolbarButtonSize((Integer) bs.getValue());
if (is != null) config.setToolbarIconSize((Integer) is.getValue());
} catch (Exception ignore) {}
}
// Save external editor path
if (externalEditorField != null) {
config.setExternalEditorPath(externalEditorField.getText());
}
// Persist config and notify caller
config.saveConfig();
if (onChange != null) onChange.run();
dispose();
});
JButton cancel = new JButton("Cancel");
cancel.addActionListener(e -> {
// Restore original values
config.setBackgroundColor(originalBg);
config.setSelectionColor(originalSel);
config.setMarkedColor(originalMark);
config.setFolderColor(originalFolder);
config.setGlobalFont(originalGlobalFont);
config.setEditorFont(originalEditorFont);
config.setExternalEditorPath(originalExternalEditorPath);
config.setToolbarButtonSize(originalToolbarButtonSize);
config.setToolbarIconSize(originalToolbarIconSize);
// Notify UI to revert changes
if (onChange != null) onChange.run();
dispose();
});
btns.add(ok);
btns.add(cancel);
add(btns, BorderLayout.SOUTH);
}
private JPanel buildAppearancePanel() {
JPanel p = new JPanel(new BorderLayout(8, 8));
JPanel grid = new JPanel(new GridBagLayout());
grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(4, 4, 4, 4);
gbc.weightx = 1.0;
int row = 0;
// Application font
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Application font:"), gbc);
appearanceFontBtn = new JButton(getFontDescription(config.getGlobalFont()));
appearanceFontBtn.addActionListener(e -> {
Font nf = FontChooserDialog.showDialog(this, config.getGlobalFont());
if (nf != null) {
config.setGlobalFont(nf);
appearanceFontBtn.setText(getFontDescription(nf));
if (onChange != null) onChange.run();
}
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(appearanceFontBtn, gbc);
// Background color
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Background color:"), gbc);
appearanceBgBtn = createColorButton(config.getBackgroundColor(), "Choose background color", c -> {
config.setBackgroundColor(c);
if (onChange != null) onChange.run();
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(appearanceBgBtn, gbc);
// Selection color
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Selection color:"), gbc);
appearanceSelBtn = createColorButton(config.getSelectionColor() != null ? config.getSelectionColor() : new Color(184, 207, 229),
"Choose selection color", c -> {
config.setSelectionColor(c);
if (onChange != null) onChange.run();
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(appearanceSelBtn, gbc);
// Marked item color
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Marked item color:"), gbc);
appearanceMarkBtn = createColorButton(config.getMarkedColor() != null ? config.getMarkedColor() : new Color(204, 153, 0),
"Choose marked item color", c -> {
config.setMarkedColor(c);
if (onChange != null) onChange.run();
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(appearanceMarkBtn, gbc);
// Folder icon color
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Folder icon color:"), gbc);
appearanceFolderBtn = createColorButton(config.getFolderColor(), "Choose folder icon color", c -> {
config.setFolderColor(c);
if (onChange != null) onChange.run();
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(appearanceFolderBtn, gbc);
p.add(grid, BorderLayout.NORTH);
panels.put("Appearance", p);
return p;
}
private JButton createColorButton(Color initialColor, String title, Consumer<Color> onColorChosen) {
JButton btn = new JButton(" ") {
@Override
public void paintComponent(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
if (getModel().isRollover()) {
g.setColor(new Color(255, 255, 255, 60));
g.fillRect(0, 0, getWidth(), getHeight());
}
if (getModel().isPressed()) {
g.setColor(new Color(0, 0, 0, 60));
g.fillRect(0, 0, getWidth(), getHeight());
}
}
};
btn.setOpaque(true);
btn.setContentAreaFilled(false);
btn.setFocusPainted(false);
btn.setBorder(BorderFactory.createLineBorder(Color.GRAY));
btn.setPreferredSize(new Dimension(80, 25));
btn.setBackground(initialColor != null ? initialColor : UIManager.getColor("Panel.background"));
btn.addActionListener(e -> {
Color chosen = JColorChooser.showDialog(this, title, btn.getBackground());
if (chosen != null) {
btn.setBackground(chosen);
onColorChosen.accept(chosen);
}
});
return btn;
}
private JPanel buildEditorPanel() {
JPanel p = new JPanel(new BorderLayout(8, 8));
JPanel grid = new JPanel(new GridBagLayout());
grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(4, 4, 4, 4);
gbc.weightx = 1.0;
int row = 0;
// Editor font
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Editor font:"), gbc);
editorFontBtn = new JButton(getFontDescription(config.getEditorFont()));
editorFontBtn.addActionListener(e -> {
Font nf = FontChooserDialog.showDialog(this, config.getEditorFont());
if (nf != null) {
config.setEditorFont(nf);
editorFontBtn.setText(getFontDescription(nf));
if (onChange != null) onChange.run();
}
});
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(editorFontBtn, gbc);
// External editor path
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("External editor:"), gbc);
JPanel extPanel = new JPanel(new BorderLayout(6, 0));
externalEditorField = new JTextField(config.getExternalEditorPath());
JButton browse = new JButton("Browse...");
browse.addActionListener(e -> {
JFileChooser fc = new JFileChooser();
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
fc.setDialogTitle("Select external editor executable");
if (!externalEditorField.getText().isEmpty()) {
File f = new File(externalEditorField.getText());
if (f.exists()) fc.setSelectedFile(f);
}
int r = fc.showOpenDialog(this);
if (r == JFileChooser.APPROVE_OPTION) {
File sel = fc.getSelectedFile();
externalEditorField.setText(sel.getAbsolutePath());
config.setExternalEditorPath(sel.getAbsolutePath());
// config.saveConfig() will be called when OK is pressed
if (onChange != null) onChange.run();
}
});
extPanel.add(externalEditorField, BorderLayout.CENTER);
extPanel.add(browse, BorderLayout.EAST);
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(extPanel, gbc);
p.add(grid, BorderLayout.NORTH);
panels.put("Editor", p);
return p;
}
private JPanel buildSortingPanel() {
JPanel p = new JPanel(new BorderLayout(8, 8));
JPanel grid = new JPanel();
grid.setLayout(new BoxLayout(grid, BoxLayout.Y_AXIS));
grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
// Helper to add a label above a control for single-column layout
java.util.function.BiConsumer<String, JComponent> addLabeled = (labelText, comp) -> {
JPanel row = new JPanel();
row.setLayout(new BoxLayout(row, BoxLayout.Y_AXIS));
JLabel lbl = new JLabel(labelText);
lbl.setAlignmentX(Component.LEFT_ALIGNMENT);
comp.setAlignmentX(Component.LEFT_ALIGNMENT);
row.add(lbl);
row.add(Box.createVerticalStrut(4));
row.add(comp);
row.setAlignmentX(Component.LEFT_ALIGNMENT);
grid.add(row);
grid.add(Box.createVerticalStrut(8));
};
JComboBox<String> primaryField = new JComboBox<>(new String[]{"Name", "Size", "Date"});
JComboBox<String> primaryOrder = new JComboBox<>(new String[]{"Ascending", "Descending"});
JPanel primaryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
primaryPanel.add(primaryField);
primaryPanel.add(primaryOrder);
addLabeled.accept("Primary sort:", primaryPanel);
JComboBox<String> secondaryField = new JComboBox<>(new String[]{"(none)", "Name", "Size", "Date"});
JComboBox<String> secondaryOrder = new JComboBox<>(new String[]{"Ascending", "Descending"});
JPanel secondaryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
secondaryPanel.add(secondaryField);
secondaryPanel.add(secondaryOrder);
addLabeled.accept("Secondary sort:", secondaryPanel);
JComboBox<String> tertiaryField = new JComboBox<>(new String[]{"(none)", "Name", "Size", "Date"});
JComboBox<String> tertiaryOrder = new JComboBox<>(new String[]{"Ascending", "Descending"});
JPanel tertiaryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
tertiaryPanel.add(tertiaryField);
tertiaryPanel.add(tertiaryOrder);
addLabeled.accept("Tertiary sort:", tertiaryPanel);
// Additional sorting options (each as own labeled row)
JComboBox<String> hiddenOrder = new JComboBox<>(new String[]{"Hidden last", "Hidden first"});
hiddenOrder.setSelectedIndex(config.getHiddenFilesLast() ? 0 : 1);
addLabeled.accept("Hidden files:", hiddenOrder);
JCheckBox uppercasePriority = new JCheckBox("Prefer uppercase first");
uppercasePriority.setSelected(config.getUppercasePriority());
addLabeled.accept("Uppercase priority:", uppercasePriority);
JCheckBox numericAware = new JCheckBox("Enable natural numeric sorting");
numericAware.setSelected(config.getNumericSortEnabled());
addLabeled.accept("Numeric-aware names:", numericAware);
JCheckBox ignoreLeadingDot = new JCheckBox("Ignore leading dot in names (treat '.name' as 'name')");
ignoreLeadingDot.setSelected(config.getIgnoreLeadingDot());
addLabeled.accept("Ignore leading dot:", ignoreLeadingDot);
// Load existing criteria from config
java.util.List<String> crit = config.getMultipleSortCriteria();
if (crit != null && !crit.isEmpty()) {
try {
// parse strings like "name:asc"
if (crit.size() > 0) {
String[] parts = crit.get(0).split(":");
if (parts.length >= 1) primaryField.setSelectedItem(capitalize(parts[0]));
if (parts.length == 2 && parts[1].equalsIgnoreCase("desc")) primaryOrder.setSelectedItem("Descending");
}
if (crit.size() > 1) {
String[] parts = crit.get(1).split(":");
if (parts.length >= 1) secondaryField.setSelectedItem("(" + parts[0] + ")");
if (parts.length == 2 && parts[1].equalsIgnoreCase("desc")) secondaryOrder.setSelectedItem("Descending");
}
if (crit.size() > 2) {
String[] parts = crit.get(2).split(":");
if (parts.length >= 1) tertiaryField.setSelectedItem("(" + parts[0] + ")");
if (parts.length == 2 && parts[1].equalsIgnoreCase("desc")) tertiaryOrder.setSelectedItem("Descending");
}
} catch (Exception ignore) {}
}
p.add(grid, BorderLayout.NORTH);
// Save action will be done on OK; store controls in panels map so OK handler can read them
JPanel holder = new JPanel();
holder.putClientProperty("primaryField", primaryField);
holder.putClientProperty("primaryOrder", primaryOrder);
holder.putClientProperty("secondaryField", secondaryField);
holder.putClientProperty("secondaryOrder", secondaryOrder);
holder.putClientProperty("tertiaryField", tertiaryField);
holder.putClientProperty("tertiaryOrder", tertiaryOrder);
holder.putClientProperty("hiddenOrder", hiddenOrder);
holder.putClientProperty("uppercasePriority", uppercasePriority);
holder.putClientProperty("numericAware", numericAware);
holder.putClientProperty("ignoreLeadingDot", ignoreLeadingDot);
panels.put("Sorting", holder);
return p;
}
private JPanel buildToolbarPanel() {
JPanel p = new JPanel(new BorderLayout(8, 8));
JPanel grid = new JPanel(new GridBagLayout());
grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(4, 4, 4, 4);
gbc.weightx = 1.0;
int row = 0;
// Button size
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Button size (px):"), gbc);
JSpinner buttonSizeSpinner = new JSpinner(new SpinnerNumberModel(config.getToolbarButtonSize(), 16, 128, 1));
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(buttonSizeSpinner, gbc);
// Icon size
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Icon size (px):"), gbc);
JSpinner iconSizeSpinner = new JSpinner(new SpinnerNumberModel(config.getToolbarIconSize(), 16, 128, 1));
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(iconSizeSpinner, gbc);
p.add(grid, BorderLayout.NORTH);
// Save action will be done on OK; store controls in a holder
JPanel holder = new JPanel();
holder.putClientProperty("buttonSize", buttonSizeSpinner);
holder.putClientProperty("iconSize", iconSizeSpinner);
panels.put("Toolbar", 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();
}
private String getFontDescription(Font f) {
if (f == null) return "(default)";
return String.format("%s %dpt", f.getName(), f.getSize());
}
}