diff --git a/src/main/java/com/kfmanager/config/AppConfig.java b/src/main/java/com/kfmanager/config/AppConfig.java index 29ae206..af35b71 100644 --- a/src/main/java/com/kfmanager/config/AppConfig.java +++ b/src/main/java/com/kfmanager/config/AppConfig.java @@ -321,6 +321,72 @@ public class AppConfig { public void setDefaultSortAscending(boolean asc) { properties.setProperty("global.sort.ascending", String.valueOf(asc)); } + + // Hidden files ordering: true = hidden after visible (default true) + public boolean getHiddenFilesLast() { + return Boolean.parseBoolean(properties.getProperty("global.sort.hidden.last", "true")); + } + + public void setHiddenFilesLast(boolean last) { + properties.setProperty("global.sort.hidden.last", String.valueOf(last)); + } + + // Uppercase preference: when names equal ignoring case, prefer uppercase first + public boolean getUppercasePriority() { + return Boolean.parseBoolean(properties.getProperty("global.sort.uppercase.priority", "true")); + } + + public void setUppercasePriority(boolean priority) { + properties.setProperty("global.sort.uppercase.priority", String.valueOf(priority)); + } + + // Numeric-aware name sorting (natural sort): default true + public boolean getNumericSortEnabled() { + return Boolean.parseBoolean(properties.getProperty("global.sort.numeric.enabled", "true")); + } + + public void setNumericSortEnabled(boolean enabled) { + properties.setProperty("global.sort.numeric.enabled", String.valueOf(enabled)); + } + + // Ignore leading dot in names when sorting (treat ".name" as "name") + public boolean getIgnoreLeadingDot() { + return Boolean.parseBoolean(properties.getProperty("global.sort.ignore.leadingdot", "false")); + } + + public void setIgnoreLeadingDot(boolean enabled) { + properties.setProperty("global.sort.ignore.leadingdot", String.valueOf(enabled)); + } + + // -- Multiple sort criteria persistence + public java.util.List getMultipleSortCriteria() { + java.util.List list = new java.util.ArrayList<>(); + int count = Integer.parseInt(properties.getProperty("global.sort.criteria.count", "0")); + for (int i = 0; i < count; i++) { + String v = properties.getProperty("global.sort.criteria." + i, null); + if (v != null && !v.isEmpty()) list.add(v); + } + return list; + } + + public void setMultipleSortCriteria(java.util.List criteria) { + if (criteria == null || criteria.isEmpty()) { + properties.setProperty("global.sort.criteria.count", "0"); + return; + } + + int limit = Math.min(criteria.size(), 5); // cap stored entries + properties.setProperty("global.sort.criteria.count", String.valueOf(limit)); + for (int i = 0; i < limit; i++) { + properties.setProperty("global.sort.criteria." + i, criteria.get(i)); + } + + // remove any old entries beyond limit + int old = Integer.parseInt(properties.getProperty("global.sort.criteria.count", "0")); + for (int i = limit; i < old; i++) { + properties.remove("global.sort.criteria." + i); + } + } /** * Save window state diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index 492cdf8..440bce9 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -1247,8 +1247,8 @@ public class FilePanelTab extends JPanel { } else { // Preserve whatever style the base font has (do not force plain) setFont(baseFont.deriveFont(baseStyle)); - // Use a light foreground color for better contrast on dark backgrounds - setForeground(new Color(240, 240, 240)); + // Use a white foreground color for better contrast on dark backgrounds + setForeground(new Color(255, 255, 255)); } // Zobrazit ikonu pro názvy souborů @@ -1414,7 +1414,7 @@ public class FilePanelTab extends JPanel { java.util.List items = tableModel.items; if (items.isEmpty()) return; - java.util.Comparator comp; + java.util.Comparator comp; switch (column) { case 1: // size comp = (a, b) -> { @@ -1435,11 +1435,7 @@ public class FilePanelTab extends JPanel { }; break; default: // name - comp = (a, b) -> { - boolean da = a.isDirectory(), db = b.isDirectory(); - if (da != db) return da ? -1 : 1; - return a.getName().compareToIgnoreCase(b.getName()); - }; + comp = (a, b) -> compareFileItemsByName(a, b); break; } @@ -1461,6 +1457,159 @@ public class FilePanelTab extends JPanel { }); } + /** + * Compare two FileItem objects by name with the following enhancements: + * - directories come before files + * - visible files come before hidden files + * - natural numeric-aware comparison ("file2" < "file10") + * - when names are equal ignoring case, prefer uppercase letters earlier (so "Apple" before "apple") + */ + private int compareFileItemsByName(FileItem a, FileItem b) { + if (a == b) return 0; + // Directories first + boolean da = a.isDirectory(), db = b.isDirectory(); + if (da != db) return da ? -1 : 1; + + // Hidden files ordering based on config (default: hidden last) + boolean hiddenLast = true; + try { + if (persistedConfig != null) hiddenLast = persistedConfig.getHiddenFilesLast(); + } catch (Exception ignore) {} + try { + boolean ha = a.getFile() != null && a.getFile().isHidden(); + boolean hb = b.getFile() != null && b.getFile().isHidden(); + if (ha != hb) return hiddenLast ? (ha ? 1 : -1) : (ha ? -1 : 1); + } catch (Exception ignore) {} + + String s1 = a.getName() != null ? a.getName() : ""; + String s2 = b.getName() != null ? b.getName() : ""; + + // Optionally ignore leading dots (treat ".name" as "name") based on config + try { + if (persistedConfig != null && persistedConfig.getIgnoreLeadingDot()) { + s1 = s1.replaceFirst("^\\.+", ""); + s2 = s2.replaceFirst("^\\.+", ""); + } + } catch (Exception ignore) {} + + // Numeric-aware vs simple compare based on config + boolean numericEnabled = true; + try { if (persistedConfig != null) numericEnabled = persistedConfig.getNumericSortEnabled(); } catch (Exception ignore) {} + + if (numericEnabled) { + // Use natural compare; uppercase preference handled inside + return naturalCompareWithUppercasePreference(s1, s2); + } else { + // simple case-insensitive compare, optionally uppercase preference + int ci = s1.compareToIgnoreCase(s2); + if (ci != 0) return ci; + boolean uppercasePref = true; + try { if (persistedConfig != null) uppercasePref = persistedConfig.getUppercasePriority(); } catch (Exception ignore) {} + if (uppercasePref) { + // prefer uppercase earlier + int len = Math.min(s1.length(), s2.length()); + for (int k = 0; k < len; k++) { + char aChar = s1.charAt(k); + char bChar = s2.charAt(k); + if (aChar == bChar) continue; + char la = Character.toLowerCase(aChar); + char lb = Character.toLowerCase(bChar); + if (la != lb) return la - lb; + boolean ua = Character.isUpperCase(aChar); + boolean ub = Character.isUpperCase(bChar); + if (ua != ub) return ua ? -1 : 1; + } + return s1.length() - s2.length(); + } else { + return s1.compareTo(s2); + } + } + } + + /** + * Natural compare of two strings with numeric awareness and uppercase preference. + */ + private int naturalCompareWithUppercasePreference(String s1, String s2) { + int i = 0, j = 0; + int n1 = s1.length(), n2 = s2.length(); + + while (i < n1 && j < n2) { + char c1 = s1.charAt(i); + char c2 = s2.charAt(j); + + // If both are digits, compare whole number sequences numerically + if (Character.isDigit(c1) && Character.isDigit(c2)) { + int start1 = i, start2 = j; + while (i < n1 && Character.isDigit(s1.charAt(i))) i++; + while (j < n2 && Character.isDigit(s2.charAt(j))) j++; + String num1 = s1.substring(start1, i); + String num2 = s2.substring(start2, j); + + // Remove leading zeros for numeric comparison, but keep length for tie-break + String nz1 = num1.replaceFirst("^0+(?!$)", ""); + String nz2 = num2.replaceFirst("^0+(?!$)", ""); + if (nz1.length() != nz2.length()) return nz1.length() - nz2.length(); + int cmp = nz1.compareTo(nz2); + if (cmp != 0) return cmp; + // If equal numerically, shorter original with fewer leading zeros first + if (num1.length() != num2.length()) return num1.length() - num2.length(); + continue; + } + + // Non-digit comparison: case-insensitive first + StringBuilder sb1 = new StringBuilder(); + StringBuilder sb2 = new StringBuilder(); + int ti = i, tj = j; + while (ti < n1 && !Character.isDigit(s1.charAt(ti))) { + sb1.append(s1.charAt(ti)); + ti++; + } + while (tj < n2 && !Character.isDigit(s2.charAt(tj))) { + sb2.append(s2.charAt(tj)); + tj++; + } + + String part1 = sb1.toString(); + String part2 = sb2.toString(); + + int ci = compareStringPartWithUppercasePreference(part1, part2); + if (ci != 0) return ci; + + i = ti; + j = tj; + } + + // If one string ended earlier, shorter one is first + if (i < n1) return 1; // s2 ended, s1 longer -> s2 first + if (j < n2) return -1; + return 0; + } + + /** + * Compare two non-digit string parts: case-insensitive, but if equal ignoring case, + * prefer the one with uppercase letters earlier. + */ + private int compareStringPartWithUppercasePreference(String p1, String p2) { + int ci = p1.compareToIgnoreCase(p2); + if (ci != 0) return ci; + // If equal ignoring case, prefer uppercase earlier + int len = Math.min(p1.length(), p2.length()); + for (int k = 0; k < len; k++) { + char a = p1.charAt(k); + char b = p2.charAt(k); + if (a == b) continue; + char la = Character.toLowerCase(a); + char lb = Character.toLowerCase(b); + if (la != lb) return la - lb; // different letters + // same letter ignoring case, decide by uppercase preference + boolean ua = Character.isUpperCase(a); + boolean ub = Character.isUpperCase(b); + if (ua != ub) return ua ? -1 : 1; // uppercase comes first + } + // If equal up to min length, shorter one first + return p1.length() - p2.length(); + } + /** Header renderer that decorates the column title with an arrow for the sort column/direction */ private class SortHeaderRenderer implements javax.swing.table.TableCellRenderer { private final javax.swing.table.TableCellRenderer delegate; @@ -1495,19 +1644,92 @@ public class FilePanelTab extends JPanel { this.persistedConfig = cfg; // Apply persisted sort if present if (cfg != null) { - int col = cfg.getDefaultSortColumn(); - boolean asc = cfg.getDefaultSortAscending(); - if (col >= 0) { - this.sortColumn = col; - this.sortAscending = asc; - sortItemsByColumn(sortColumn, sortAscending); - SwingUtilities.invokeLater(() -> { - tableModel.fireTableDataChanged(); - if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); - }); + java.util.List multi = cfg.getMultipleSortCriteria(); + if (multi != null && !multi.isEmpty()) { + applyMultipleSortCriteria(multi); + } else { + int col = cfg.getDefaultSortColumn(); + boolean asc = cfg.getDefaultSortAscending(); + if (col >= 0) { + this.sortColumn = col; + this.sortAscending = asc; + sortItemsByColumn(sortColumn, sortAscending); + SwingUtilities.invokeLater(() -> { + tableModel.fireTableDataChanged(); + if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); + }); + } } } } + + /** + * Apply a list of composite sort criteria in order (e.g. "name:asc", "size:desc"). + */ + private void applyMultipleSortCriteria(java.util.List criteria) { + if (criteria == null || criteria.isEmpty()) return; + if (tableModel == null || tableModel.items == null) return; + + java.util.Comparator comp = null; + + for (String c : criteria) { + if (c == null || c.trim().isEmpty()) continue; + String[] parts = c.split(":"); + String field = parts[0].trim().toLowerCase(); + boolean asc = true; + if (parts.length > 1 && parts[1].trim().equalsIgnoreCase("desc")) asc = false; + + java.util.Comparator partComp; + switch (field) { + case "size": + partComp = (a, b) -> { + boolean da = a.isDirectory(), db = b.isDirectory(); + if (da != db) return da ? -1 : 1; + if (da && db) return a.getName().compareToIgnoreCase(b.getName()); + int r = Long.compare(a.getSize(), b.getSize()); + if (r == 0) r = a.getName().compareToIgnoreCase(b.getName()); + return r; + }; + break; + case "date": + partComp = (a, b) -> { + int r = Long.compare(a.getModified().getTime(), b.getModified().getTime()); + if (r == 0) r = a.getName().compareToIgnoreCase(b.getName()); + return r; + }; + break; + default: + partComp = (a, b) -> { + boolean da = a.isDirectory(), db = b.isDirectory(); + if (da != db) return da ? -1 : 1; + return a.getName().compareToIgnoreCase(b.getName()); + }; + break; + } + + if (!asc) partComp = partComp.reversed(); + + if (comp == null) comp = partComp; + else comp = comp.thenComparing(partComp); + } + + if (comp == null) return; + + java.util.List items = tableModel.items; + items.sort(comp); + + SwingUtilities.invokeLater(() -> { + if (viewMode == ViewMode.BRIEF) { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + } else { + tableModel.fireTableDataChanged(); + } + updateColumnRenderers(); + updateColumnWidths(); + if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); + }); + } private String formatSize(long size) { if (size < 1024) { diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java index 83f9426..1364b38 100644 --- a/src/main/java/com/kfmanager/ui/MainWindow.java +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -390,6 +390,16 @@ public class MainWindow extends JFrame { if (leftPanel != null) leftPanel.applyMarkedColor(mark); if (rightPanel != null) rightPanel.applyMarkedColor(mark); } + + // Re-propagate AppConfig to panels so they can pick up sorting and other config changes + SwingUtilities.invokeLater(() -> { + try { + if (leftPanel != null) leftPanel.setAppConfig(config); + } catch (Exception ignore) {} + try { + if (rightPanel != null) rightPanel.setAppConfig(config); + } catch (Exception ignore) {} + }); } /** diff --git a/src/main/java/com/kfmanager/ui/SettingsDialog.java b/src/main/java/com/kfmanager/ui/SettingsDialog.java index 6a76870..0ba69fc 100644 --- a/src/main/java/com/kfmanager/ui/SettingsDialog.java +++ b/src/main/java/com/kfmanager/ui/SettingsDialog.java @@ -40,9 +40,10 @@ public class SettingsDialog extends JDialog { setLocationRelativeTo(parent); // Left: categories - DefaultListModel model = new DefaultListModel<>(); - model.addElement("Appearance"); - model.addElement("Editor"); + DefaultListModel model = new DefaultListModel<>(); + model.addElement("Appearance"); + model.addElement("Editor"); + model.addElement("Sorting"); categoryList = new JList<>(model); categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); categoryList.setSelectedIndex(0); @@ -51,8 +52,9 @@ public class SettingsDialog extends JDialog { cards = new JPanel(cardLayout); // Build category panels - cards.add(buildAppearancePanel(), "Appearance"); - cards.add(buildEditorPanel(), "Editor"); + cards.add(buildAppearancePanel(), "Appearance"); + cards.add(buildEditorPanel(), "Editor"); + cards.add(buildSortingPanel(), "Sorting"); categoryList.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { @@ -71,8 +73,69 @@ public class SettingsDialog extends JDialog { JPanel btns = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton ok = new JButton("OK"); ok.addActionListener(e -> { - // Persist is handled by individual actions; ensure saved and close + // 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) {} + } + + // Persist config and notify caller config.saveConfig(); + if (onChange != null) onChange.run(); dispose(); }); JButton cancel = new JButton("Cancel"); @@ -192,6 +255,112 @@ public class SettingsDialog extends JDialog { 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 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());