sorting fixes

This commit is contained in:
rdavidek 2026-03-08 19:53:24 +01:00
parent 3bb18fc03e
commit 2ce0bd2ee2
5 changed files with 231 additions and 319 deletions

View File

@ -563,15 +563,6 @@ 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"));
@ -599,6 +590,15 @@ public class AppConfig {
properties.setProperty("global.sort.ignore.leadingdot", String.valueOf(enabled));
}
// Ignore leading dollar sign in names when sorting (treat "$name" as "name")
public boolean getIgnoreLeadingDollar() {
return Boolean.parseBoolean(properties.getProperty("global.sort.ignore.leadingdollar", "false"));
}
public void setIgnoreLeadingDollar(boolean enabled) {
properties.setProperty("global.sort.ignore.leadingdollar", String.valueOf(enabled));
}
public int getAutoRefreshInterval() {
return Integer.parseInt(properties.getProperty("panel.autoRefreshInterval", "2000"));
}

View File

@ -296,6 +296,30 @@ public class FilePanel extends JPanel {
} catch (Exception ignore) {}
});
}
/**
* Return true when the focused component belongs to the drive selector.
*/
public boolean isDriveSelectorFocusOwner(Component focusedComponent) {
if (driveCombo == null || focusedComponent == null) {
return false;
}
return focusedComponent == driveCombo || SwingUtilities.isDescendingFrom(focusedComponent, driveCombo);
}
/**
* Return true when focus should not activate this panel (utility UI controls).
*/
public boolean isNonActivatingFocusOwner(Component focusedComponent) {
if (focusedComponent == null) {
return false;
}
if (isDriveSelectorFocusOwner(focusedComponent)) {
return true;
}
FilePanelTab currentTab = getCurrentTab();
return currentTab != null && currentTab.isSortControlFocusOwner(focusedComponent);
}
/**
* Add a new tab with a directory

View File

@ -9,6 +9,7 @@ import cz.kamma.kfmanager.service.FileOperations;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
@ -292,6 +293,28 @@ public class FilePanelTab extends JPanel {
fileTable.repaint();
}
}
/**
* Return true when focus belongs to one of the BRIEF sorting controls.
*/
public boolean isSortControlFocusOwner(Component focusedComponent) {
if (focusedComponent == null) {
return false;
}
if (briefSortPanel != null && briefSortPanel.isVisible() && SwingUtilities.isDescendingFrom(focusedComponent, briefSortPanel)) {
return true;
}
JTableHeader header = fileTable != null ? fileTable.getTableHeader() : null;
return header != null && SwingUtilities.isDescendingFrom(focusedComponent, header);
}
private void requestTableFocusIfActive() {
if (active && fileTable != null) {
fileTable.requestFocusInWindow();
}
}
private void initComponents() {
setLayout(new BorderLayout());
@ -851,7 +874,7 @@ public class FilePanelTab extends JPanel {
persistedConfig.setDefaultSortAscending(sortAscending);
persistedConfig.saveConfig();
}
fileTable.requestFocus();
requestTableFocusIfActive();
});
sortByDateButton.addActionListener(e -> {
@ -870,7 +893,7 @@ public class FilePanelTab extends JPanel {
persistedConfig.setDefaultSortAscending(sortAscending);
persistedConfig.saveConfig();
}
fileTable.requestFocus();
requestTableFocusIfActive();
});
sortBySizeButton.addActionListener(e -> {
@ -889,7 +912,7 @@ public class FilePanelTab extends JPanel {
persistedConfig.setDefaultSortAscending(sortAscending);
persistedConfig.saveConfig();
}
fileTable.requestFocus();
requestTableFocusIfActive();
});
add(briefSortPanel, BorderLayout.NORTH);
@ -1387,7 +1410,7 @@ public class FilePanelTab extends JPanel {
// Clean up expired timestamps
changeTimestamps.entrySet().removeIf(entry -> now - entry.getValue() > CHANGE_HIGHLIGHT_DURATION);
if (isSameContent(newItems, tableModel.items)) {
if (isSameContentIgnoringOrder(newItems, tableModel.items)) {
// Nothing changed on disk since last refresh.
// Check if we need to clear highlight for items whose duration expired.
boolean repainNeeded = false;
@ -1442,7 +1465,7 @@ public class FilePanelTab extends JPanel {
}
final List<FileItem> itemsToLoad = newItems;
final boolean contentChanged = !isSameContent(newItems, tableModel.items);
final boolean contentChanged = !isSameContentIgnoringOrder(newItems, tableModel.items);
loadDirectory(currentDirectory, itemsToLoad, false, requestFocus, () -> {
// Restore marks and set recentlyChanged flag based on active timestamps
@ -1547,6 +1570,23 @@ public class FilePanelTab extends JPanel {
return true;
}
private boolean isSameContentIgnoringOrder(List<FileItem> list1, List<FileItem> list2) {
if (list1.size() != list2.size()) return false;
Map<String, FileItem> byName = new HashMap<>();
for (FileItem item : list1) {
byName.put(item.getName(), item);
}
for (FileItem item : list2) {
FileItem other = byName.get(item.getName());
if (other == null || !other.isSameAs(item)) {
return false;
}
}
return true;
}
/**
* Cleanup previous archive temp dir when navigating away from it.
*/
@ -3236,11 +3276,92 @@ public class FilePanelTab extends JPanel {
}
/**
* Remove leading and trailing $ characters for sorting purposes.
* Normalize name for sorting (currently optional leading '$' ignore).
*/
private String getCleanNameForSorting(String name) {
if (name == null) return "";
return name.replaceAll("^\\$+|\\$+$", "");
boolean ignoreLeadingDollar = false;
try {
if (persistedConfig != null) {
ignoreLeadingDollar = persistedConfig.getIgnoreLeadingDollar();
}
} catch (Exception ignore) {}
if (ignoreLeadingDollar) {
return name.replaceFirst("^\\$+", "");
}
return name;
}
private int compareNamesWithConfiguredOptions(FileItem a, FileItem b) {
String s1 = getCleanNameForSorting(a.getName());
String s2 = getCleanNameForSorting(b.getName());
try {
if (persistedConfig != null && persistedConfig.getIgnoreLeadingDot()) {
s1 = s1.replaceFirst("^\\.+", "");
s2 = s2.replaceFirst("^\\.+", "");
}
} catch (Exception ignore) {}
boolean uppercasePref = true;
try {
if (persistedConfig != null) uppercasePref = persistedConfig.getUppercasePriority();
} catch (Exception ignore) {}
boolean numericEnabled = true;
try {
if (persistedConfig != null) numericEnabled = persistedConfig.getNumericSortEnabled();
} catch (Exception ignore) {}
if (numericEnabled) {
return naturalCompareWithUppercasePreference(s1, s2, uppercasePref);
}
int ci = s1.compareToIgnoreCase(s2);
if (ci != 0) return ci;
if (!uppercasePref) {
return s1.compareTo(s2);
}
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();
}
private int compareByColumnWithConfig(FileItem a, FileItem b, int column, boolean asc) {
boolean da = a.isDirectory();
boolean db = b.isDirectory();
if (da != db) return da ? -1 : 1;
int valueCmp;
switch (column) {
case 1:
valueCmp = Long.compare(a.getSize(), b.getSize());
break;
case 2:
valueCmp = Long.compare(a.getModified().getTime(), b.getModified().getTime());
break;
default:
valueCmp = compareNamesWithConfiguredOptions(a, b);
break;
}
if (!asc) valueCmp = -valueCmp;
if (valueCmp != 0) return valueCmp;
return compareNamesWithConfiguredOptions(a, b);
}
private void sortItemsByColumn(int column, boolean asc) {
@ -3259,57 +3380,16 @@ public class FilePanelTab extends JPanel {
parentDir = items.remove(0);
}
// Separate directories and files
java.util.List<FileItem> directories = new ArrayList<>();
java.util.List<FileItem> files = new ArrayList<>();
for (FileItem item : items) {
if (item.isDirectory()) {
directories.add(item);
} else {
files.add(item);
}
}
java.util.Comparator<FileItem> comp = (a, b) -> compareByColumnWithConfig(a, b, column, asc);
java.util.List<FileItem> sorted = new ArrayList<>(items);
sorted.sort(comp);
java.util.Comparator<FileItem> comp;
switch (column) {
case 1: // size
comp = (a, b) -> {
int r = Long.compare(a.getSize(), b.getSize());
if (r == 0) r = getCleanNameForSorting(a.getName()).compareToIgnoreCase(getCleanNameForSorting(b.getName()));
return r;
};
break;
case 2: // date
// Sort by modification time
comp = (a, b) -> {
int r = Long.compare(a.getModified().getTime(), b.getModified().getTime());
if (r == 0) r = getCleanNameForSorting(a.getName()).compareToIgnoreCase(getCleanNameForSorting(b.getName()));
return r;
};
break;
default: // name
comp = (a, b) -> {
String s1 = getCleanNameForSorting(a.getName());
String s2 = getCleanNameForSorting(b.getName());
return s1.compareToIgnoreCase(s2);
};
break;
}
if (!asc) comp = comp.reversed();
// Sort both lists separately
directories.sort(comp);
files.sort(comp);
// Clear and rebuild items list: directories first, then files
// Rebuild items list and keep parent directory entry at top.
items.clear();
if (parentDir != null) {
items.add(parentDir);
}
items.addAll(directories);
items.addAll(files);
items.addAll(sorted);
// Refresh table on EDT
SwingUtilities.invokeLater(() -> {
@ -3345,79 +3425,10 @@ 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) {
private int naturalCompareWithUppercasePreference(String s1, String s2, boolean uppercasePref) {
int i = 0, j = 0;
int n1 = s1.length(), n2 = s2.length();
@ -3460,7 +3471,7 @@ public class FilePanelTab extends JPanel {
String part1 = sb1.toString();
String part2 = sb2.toString();
int ci = compareStringPartWithUppercasePreference(part1, part2);
int ci = compareStringPartWithUppercasePreference(part1, part2, uppercasePref);
if (ci != 0) return ci;
i = ti;
@ -3477,9 +3488,12 @@ public class FilePanelTab extends JPanel {
* 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) {
private int compareStringPartWithUppercasePreference(String p1, String p2, boolean uppercasePref) {
int ci = p1.compareToIgnoreCase(p2);
if (ci != 0) return ci;
if (!uppercasePref) {
return p1.compareTo(p2);
}
// If equal ignoring case, prefer uppercase earlier
int len = Math.min(p1.length(), p2.length());
for (int k = 0; k < len; k++) {
@ -3530,26 +3544,20 @@ public class FilePanelTab extends JPanel {
public void setAppConfig(cz.kamma.kfmanager.config.AppConfig cfg) {
this.persistedConfig = cfg;
iconCache.clear();
// Apply persisted sort if present - try global.sort.column first, then MultipleSortCriteria
// Apply persisted sort settings from Sorting configuration.
if (cfg != null) {
int col = cfg.getDefaultSortColumn();
boolean asc = cfg.getDefaultSortAscending();
if (col >= 0) {
// Use global sort column setting
this.sortColumn = col;
this.sortAscending = asc;
// If items are already loaded, sort them
if (tableModel != null && tableModel.items != null && !tableModel.items.isEmpty()) {
sortItemsByColumn(sortColumn, sortAscending);
}
} else {
// No global sort setting, try multiple criteria
java.util.List<String> multi = cfg.getMultipleSortCriteria();
if (multi != null && !multi.isEmpty()) {
applyMultipleSortCriteria(multi);
}
if (col < 0 || col > 2) {
col = 0;
}
this.sortColumn = col;
this.sortAscending = asc;
if (tableModel != null && tableModel.items != null && !tableModel.items.isEmpty()) {
sortItemsByColumn(sortColumn, sortAscending);
}
}
@ -3557,74 +3565,6 @@ public class FilePanelTab extends JPanel {
updateSortButtonsDisplay();
}
/**
* 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();
});
}
// Getters
public JTable getFileTable() {
return fileTable;

View File

@ -269,12 +269,18 @@ public class MainWindow extends JFrame {
Component focused = (Component) evt.getNewValue();
if (focused != null) {
if (SwingUtilities.isDescendingFrom(focused, leftPanel)) {
if (activePanel != leftPanel && leftPanel.isNonActivatingFocusOwner(focused)) {
return;
}
activePanel = leftPanel;
updateActivePanelBorder();
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
updateCommandLinePrompt();
} else if (SwingUtilities.isDescendingFrom(focused, rightPanel)) {
if (activePanel != rightPanel && rightPanel.isNonActivatingFocusOwner(focused)) {
return;
}
activePanel = rightPanel;
updateActivePanelBorder();
leftPanel.getFileTable().repaint();

View File

@ -166,49 +166,31 @@ public class SettingsDialog extends JDialog {
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");
JComboBox<?> sortField = (JComboBox<?>) sortingHolder.getClientProperty("sortField");
JComboBox<?> sortOrder = (JComboBox<?>) sortingHolder.getClientProperty("sortOrder");
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);
int sortColumn = 0;
if (sortField != null && sortField.getSelectedItem() != null) {
String selected = sortField.getSelectedItem().toString();
if ("Size".equals(selected)) {
sortColumn = 1;
} else if ("Date".equals(selected)) {
sortColumn = 2;
}
}
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);
}
}
boolean ascending = sortOrder == null
|| sortOrder.getSelectedItem() == null
|| "Ascending".equals(sortOrder.getSelectedItem().toString());
config.setMultipleSortCriteria(criteria);
config.setDefaultSortColumn(sortColumn);
config.setDefaultSortAscending(ascending);
// Legacy multi-sort criteria are no longer configurable from UI.
config.setMultipleSortCriteria(java.util.Collections.emptyList());
// 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());
}
@ -219,6 +201,10 @@ public class SettingsDialog extends JDialog {
if (ignoreLeadingDot != null) {
config.setIgnoreLeadingDot(ignoreLeadingDot.isSelected());
}
JCheckBox ignoreLeadingDollar = (JCheckBox) sortingHolder.getClientProperty("ignoreLeadingDollar");
if (ignoreLeadingDollar != null) {
config.setIgnoreLeadingDollar(ignoreLeadingDollar.isSelected());
}
} catch (Exception ignore) {}
}
@ -283,7 +269,6 @@ public class SettingsDialog extends JDialog {
config.setSelectionColor(originalSel);
config.setMarkedColor(originalMark);
config.setFolderColor(originalFolder);
config.setBriefModeMaxNameLength(originalBriefMaxLen);
config.setBriefModeStartLength(originalBriefStartLen);
config.setBriefModeEndLength(originalBriefEndLen);
config.setBriefModeSeparator(originalBriefSeparator);
@ -463,15 +448,6 @@ public class SettingsDialog extends JDialog {
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Brief mode max name length:"), gbc);
JSpinner briefMaxLenSpinner = new JSpinner(new SpinnerNumberModel(config.getBriefModeMaxNameLength(), 10, 255, 1));
briefMaxLenSpinner.addChangeListener(e -> {
config.setBriefModeMaxNameLength((Integer) briefMaxLenSpinner.getValue());
if (onChange != null) onChange.run();
});
p.putClientProperty("briefMaxLen", briefMaxLenSpinner);
gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0;
grid.add(briefMaxLenSpinner, gbc);
// Brief mode start/end length
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0;
grid.add(new JLabel("Brief mode truncation (start/end):"), gbc);
@ -665,31 +641,14 @@ public class SettingsDialog extends JDialog {
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);
JComboBox<String> sortField = new JComboBox<>(new String[]{"Name", "Size", "Date"});
JComboBox<String> sortOrder = new JComboBox<>(new String[]{"Ascending", "Descending"});
JPanel sortPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
sortPanel.add(sortField);
sortPanel.add(sortOrder);
addLabeled.accept("Sort by:", sortPanel);
// 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());
@ -703,43 +662,31 @@ public class SettingsDialog extends JDialog {
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.getFirst().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) {}
JCheckBox ignoreLeadingDollar = new JCheckBox("Ignore leading $ in names (treat '$name' as 'name')");
ignoreLeadingDollar.setSelected(config.getIgnoreLeadingDollar());
addLabeled.accept("Ignore leading $:", ignoreLeadingDollar);
int defaultColumn = config.getDefaultSortColumn();
boolean defaultAsc = config.getDefaultSortAscending();
if (defaultColumn == 1) {
sortField.setSelectedItem("Size");
} else if (defaultColumn == 2) {
sortField.setSelectedItem("Date");
} else {
sortField.setSelectedItem("Name");
}
sortOrder.setSelectedItem(defaultAsc ? "Ascending" : "Descending");
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("sortField", sortField);
holder.putClientProperty("sortOrder", sortOrder);
holder.putClientProperty("uppercasePriority", uppercasePriority);
holder.putClientProperty("numericAware", numericAware);
holder.putClientProperty("ignoreLeadingDot", ignoreLeadingDot);
holder.putClientProperty("ignoreLeadingDollar", ignoreLeadingDollar);
panels.put("Sorting", holder);
return p;
@ -963,11 +910,6 @@ public class SettingsDialog extends JDialog {
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 "%s %dpt".formatted(f.getName(), f.getSize());