added sorting, not working

This commit is contained in:
Radek Davidek 2025-11-19 09:52:43 +01:00
parent 7529cb31eb
commit 5a7312eb8e
4 changed files with 491 additions and 24 deletions

View File

@ -322,6 +322,72 @@ public class AppConfig {
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<String> getMultipleSortCriteria() {
java.util.List<String> 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<String> 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
*/

View File

@ -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ů
@ -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,6 +1644,10 @@ public class FilePanelTab extends JPanel {
this.persistedConfig = cfg;
// Apply persisted sort if present
if (cfg != null) {
java.util.List<String> multi = cfg.getMultipleSortCriteria();
if (multi != null && !multi.isEmpty()) {
applyMultipleSortCriteria(multi);
} else {
int col = cfg.getDefaultSortColumn();
boolean asc = cfg.getDefaultSortAscending();
if (col >= 0) {
@ -1508,6 +1661,75 @@ public class FilePanelTab extends JPanel {
}
}
}
}
/**
* Apply a list of composite sort criteria in order (e.g. "name:asc", "size:desc").
*/
private void applyMultipleSortCriteria(java.util.List<String> criteria) {
if (criteria == null || criteria.isEmpty()) return;
if (tableModel == null || tableModel.items == null) return;
java.util.Comparator<FileItem> 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<FileItem> 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<FileItem> 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) {

View File

@ -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) {}
});
}
/**

View File

@ -43,6 +43,7 @@ public class SettingsDialog extends JDialog {
DefaultListModel<String> model = new DefaultListModel<>();
model.addElement("Appearance");
model.addElement("Editor");
model.addElement("Sorting");
categoryList = new JList<>(model);
categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
categoryList.setSelectedIndex(0);
@ -53,6 +54,7 @@ public class SettingsDialog extends JDialog {
// Build category panels
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<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) {}
}
// 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<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 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());