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 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 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 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 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 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 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 primaryField = new JComboBox<>(new String[]{"Name", "Size", "Date"}); JComboBox 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 secondaryField = new JComboBox<>(new String[]{"(none)", "Name", "Size", "Date"}); JComboBox 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 tertiaryField = new JComboBox<>(new String[]{"(none)", "Name", "Size", "Date"}); JComboBox 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 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 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()); } }