From 2eadd6ae13aa789ca1c1aab2f26d2d6efa6817f7 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:57:02 -0400 Subject: [PATCH] GP-7144 - Updated the Front End to use a natural sort for project files --- .../help/topics/Tool/ToolOptions_Dialog.htm | 11 + .../datastruct/AlphaNumericComparator.java | 29 +- .../AlphaNumericComparatorTest.java | 59 +++- .../ghidra/framework/main/FrontEndPlugin.java | 4 + .../ghidra/framework/main/FrontEndTool.java | 31 +- .../framework/main/ProjectDataPanel.java | 28 +- .../main/datatable/ProjectDataTableModel.java | 18 + .../main/datatable/ProjectDataTablePanel.java | 3 +- .../framework/main/datatree/DataTreeNode.java | 332 ++++++++++-------- .../main/datatree/DomainFileNode.java | 9 +- .../main/datatree/DomainFolderNode.java | 9 +- .../main/datatree/ProjectDataTreePanel.java | 17 +- .../java/ghidra/framework/model/Project.java | 4 +- .../ghidra/framework/model/ProjectData.java | 4 +- 14 files changed, 392 insertions(+), 166 deletions(-) diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm index 03177e4b34..6231216f08 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm @@ -464,6 +464,17 @@ This controls whether or not Ghidra will compress data being sent to the server. + + + + Use Natural Project Sort + + This controls whether to use a natural sort for + project files in the tree and table. When true, runs of digits in filenames will be + considered as a single integer value when sorting. + + + Use Notification Animation diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java index 7a17a29c12..fee1aac39a 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java @@ -23,6 +23,16 @@ import java.util.Comparator; */ public class AlphaNumericComparator implements Comparator { + private boolean caseSensitive; + + public AlphaNumericComparator() { + this.caseSensitive = true; + } + + public AlphaNumericComparator(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + @Override public int compare(String s1, String s2) { if (s1 == null || s2 == null) { @@ -66,9 +76,11 @@ public class AlphaNumericComparator implements Comparator { } else { // Compare standard characters alphabetically - if (c1 != c2) { - return Character.compare(c1, c2); + int result = compare(c1, c2); + if (result != 0) { + return result; } + i++; j++; } @@ -98,6 +110,19 @@ public class AlphaNumericComparator implements Comparator { return s1.compareTo(s2); } + private int compare(char c1, char c2) { + if (c1 == c2) { + return 0; + } + + if (!caseSensitive) { + c1 = Character.toLowerCase(c1); + c2 = Character.toLowerCase(c2); + } + + return Character.compare(c1, c2); + } + private long parseNumber(String s, int i) { long value = 0; while (i < s.length()) { diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java index cbc567e10d..afac377c4b 100644 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java @@ -26,7 +26,7 @@ import generic.test.AbstractGenericTest; public class AlphaNumericComparatorTest extends AbstractGenericTest { @Test - public void testIt() { + public void testCaseSensitive() { List unsorted = Arrays.asList( @@ -81,4 +81,61 @@ public class AlphaNumericComparatorTest extends AbstractGenericTest { assertEquals(sorted, copy); } + + @Test + public void testCaseInsensitive() { + + List unsorted = Arrays.asList( + + " file10", // Leading spaces + "file02 ", // Trailing spaces + "file 2", // Internal spaces + "file1", // Normal + " file002 ", // Leading and trailing spaces + "file 01", // Internal space before digit + "file20", + + " apple", // Leading spaces + "Apple ", // Trailing spaces + "ba nana", // Internal spaces + "a", + "alpha ", // Trailing spaces + " alphabet", // Leading spaces + "app", + "ap ple", // Internal spaces + + "0.1.0", + "0.1.9", + "1.0", + "0.2.1", + "2.1.0"); + + List sorted = Arrays.asList( + "0.1.0", + "0.1.9", + "0.2.1", + "1.0", + "2.1.0", + "a", + "alpha ", + " alphabet", + "app", + " apple", + "Apple ", + "ap ple", + "ba nana", + "file1", + "file 01", + "file 2", + "file02 ", + " file002 ", + " file10", + "file20"); + + Collections.shuffle(unsorted); + List copy = new ArrayList<>(unsorted); + Collections.sort(copy, new AlphaNumericComparator(false)); + + assertEquals(sorted, copy); + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java index d50d747127..76ab81853f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java @@ -193,6 +193,10 @@ public class FrontEndPlugin extends Plugin createToolSpecificOpenActions(); } + void setUseNaturalSort(boolean b) { + projectDataPanel.setUseNaturalSort(b); + } + protected void createToolSpecificOpenActions() { for (DockingAction action : openActions) { tool.removeAction(action); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java index ea8425f5c0..7a96a1a783 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java @@ -88,15 +88,17 @@ import help.HelpService; * manner. */ public class FrontEndTool extends PluginTool implements OptionsChangeListener { - public static final String DEFAULT_TOOL_LAUNCH_MODE = "Default Tool Launch Mode"; - public static final String AUTOMATICALLY_SAVE_TOOLS = "Automatically Save Tools"; - private static final String USE_ALERT_ANIMATION_OPTION_NAME = "Use Notification Animation"; - private static final String USE_COMBINED_ALT_GRAPH_OPTION_NAME = "Use Combined Alt Keys"; - private static final String SHOW_TOOLTIPS_OPTION_NAME = "Show Tooltips"; - private static final String BLINKING_CURSORS_OPTION_NAME = "Allow Blinking Cursors"; - private static final String ENABLE_COMPRESSED_DATABUFFER_OUTPUT = + private static final String BLINKING_CURSORS_OPTION_NAME = "Allow Blinking Cursors"; + public static final String AUTOMATICALLY_SAVE_TOOLS = "Automatically Save Tools"; + public static final String DEFAULT_TOOL_LAUNCH_MODE = "Default Tool Launch Mode"; + private static final String SHOW_TOOLTIPS_OPTION_NAME = "Show Tooltips"; + private static final String USE_COMPRESSED_DATABUFFER_OUTPUT = "Use DataBuffer Output Compression"; + private static final String USE_NATURAL_SORT = "Use Natural File Sort"; + private static final String USE_COMBINED_ALT_GRAPH_OPTION_NAME = "Use Combined Alt Keys"; + private static final String USE_ALERT_ANIMATION_OPTION_NAME = "Use Notification Animation"; + private static final Boolean ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT = true; private static final String RESTORE_PREVIOUS_PROJECT_NAME = "Restore Previous Project"; @@ -356,7 +358,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { options.registerOption(SHOW_TOOLTIPS_OPTION_NAME, true, help, "Controls the display of tooltip popup windows."); - options.registerOption(ENABLE_COMPRESSED_DATABUFFER_OUTPUT, + options.registerOption(USE_COMPRESSED_DATABUFFER_OUTPUT, ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT, help, "When enabled data buffers sent to Ghidra Server are compressed (see server " + "configuration for other direction)"); @@ -367,6 +369,9 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { options.registerOption(RESTORE_PREVIOUS_PROJECT_NAME, true, help, "Restore the previous project when Ghidra starts."); + options.registerOption(USE_NATURAL_SORT, true, help, + "Use a natural sort for program files with numeric digits treated as numeric values."); + defaultLaunchMode = options.getEnum(DEFAULT_TOOL_LAUNCH_MODE, defaultLaunchMode); boolean autoSave = options.getBoolean(AUTOMATICALLY_SAVE_TOOLS, true); @@ -382,7 +387,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { DockingUtils.setGlobalTooltipEnabledOption(showToolTips); boolean compressDataBuffers = - options.getBoolean(ENABLE_COMPRESSED_DATABUFFER_OUTPUT, + options.getBoolean(USE_COMPRESSED_DATABUFFER_OUTPUT, ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT); DataBuffer.enableCompressedSerializationOutput(compressDataBuffers); @@ -391,6 +396,9 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { boolean blink = options.getBoolean(BLINKING_CURSORS_OPTION_NAME, true); Gui.setBlinkingCursors(blink); + boolean useNaturalSort = options.getBoolean(USE_NATURAL_SORT, true); + plugin.setUseNaturalSort(useNaturalSort); + options.addOptionsChangeListener(this); } @@ -412,7 +420,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { else if (SHOW_TOOLTIPS_OPTION_NAME.equals(optionName)) { DockingUtils.setGlobalTooltipEnabledOption((Boolean) newValue); } - else if (ENABLE_COMPRESSED_DATABUFFER_OUTPUT.equals(optionName)) { + else if (USE_COMPRESSED_DATABUFFER_OUTPUT.equals(optionName)) { DataBuffer.enableCompressedSerializationOutput((Boolean) newValue); } else if (RESTORE_PREVIOUS_PROJECT_NAME.equals(optionName)) { @@ -421,6 +429,9 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { else if (BLINKING_CURSORS_OPTION_NAME.equals(optionName)) { Gui.setBlinkingCursors((Boolean) newValue); } + else if (USE_NATURAL_SORT.equals(optionName)) { + plugin.setUseNaturalSort((Boolean) newValue); + } } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java index 8108910286..74a6953b4d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java @@ -19,15 +19,17 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.event.MouseEvent; import java.net.URL; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.Map.Entry; import javax.swing.*; import docking.*; import docking.widgets.tabbedpane.DockingTabRenderer; import ghidra.framework.client.NotConnectedException; +import ghidra.framework.main.datatable.ProjectDataTableModel; import ghidra.framework.main.datatable.ProjectDataTablePanel; +import ghidra.framework.main.datatree.DataTreeNode; import ghidra.framework.main.datatree.ProjectDataTreePanel; import ghidra.framework.model.*; import ghidra.framework.options.SaveState; @@ -212,7 +214,7 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener { } try { - // TODO: addProjectView should be done in a model task + // Note: addProjectView should be done in a modal task ProjectData projectData = activeProject.addProjectView(projectView, true); if (projectData == null) { return null; // repository connection may have been cancelled @@ -365,6 +367,26 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener { } + void setUseNaturalSort(boolean b) { + + // update the view sorting code and then trigger a reload + DataTreeNode.setUseNaturalSort(b); + ProjectDataTableModel.setUseNaturalSort(b); + + // the active project + treePanel.reload(); + tablePanel.reload(); + + // read-only views + Set> entries = readOnlyViews.entrySet(); + for (Entry entry : entries) { + ProjectDataTreePanel pdtp = entry.getValue(); + if (pdtp != null) { + pdtp.reload(); + } + } + } + void setBorder(String projectName) { projectTab.setBorder(BorderFactory.createTitledBorder(BORDER_PREFIX + projectName)); treePanel.updateProjectName(projectName); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java index 7e59f43e8e..6d65634e7c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java @@ -27,12 +27,15 @@ import ghidra.util.InvalidNameException; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.datastruct.Accumulator; +import ghidra.util.datastruct.AlphaNumericComparator; import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateFileException; import ghidra.util.task.TaskMonitor; public class ProjectDataTableModel extends ThreadedTableModel { + private static boolean useNaturalSort = true; + private ProjectData projectData; private volatile int modCount; private boolean editingOn; @@ -43,6 +46,10 @@ public class ProjectDataTableModel extends ThreadedTableModel { + private static AlphaNumericComparator alphaNumericComparator = + new AlphaNumericComparator(false); + @Override public String getColumnName() { return "Name"; @@ -205,6 +215,14 @@ public class ProjectDataTableModel extends ThreadedTableModel getComparator() { + if (useNaturalSort) { + return alphaNumericComparator; + } + return super.getComparator(); + } } private class ModificationDateColumn diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java index 08968a0e11..73197db0a2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java @@ -255,14 +255,13 @@ public class ProjectDataTablePanel extends JPanel { } } - private void reload() { + public void reload() { checkCapacity(); if (!capacityExceeded) { model.reload(); } - } // load the max file count system property diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java index c9ea432621..7f1ff52363 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java @@ -17,11 +17,14 @@ package ghidra.framework.main.datatree; import java.util.*; +import javax.swing.Icon; + import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeSlowLoadingNode; import ghidra.framework.data.LinkHandler; import ghidra.framework.data.LinkHandler.LinkStatus; import ghidra.framework.model.*; +import ghidra.util.datastruct.AlphaNumericComparator; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -36,45 +39,34 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta * sort order is based upon the following comparisons in order of significance: *
    *
  1. Node type weighting. Folder and Folder-Links have equal weighting.
  2. - *
  3. Node comparison by name (see {@link DataTreeNode#compareNodeNames(String, String)}).
  4. + *
  5. Node comparison by name.
  6. *
  7. Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder * will be placed after the Folder.
  8. *
*/ enum NodeType { - FOLDER(1), FOLDER_LINK(1), FILE(2), OTHER(3); + FOLDER(1), + FOLDER_LINK(1), + FILE(2); - int weight; + private int weight; NodeType(int weight) { this.weight = weight; } - - static NodeType getNodeType(GTreeNode node) { - if (node instanceof DomainFolderNode) { - return FOLDER; - } - if (node instanceof DomainFileNode fileNode) { - return fileNode.isFolderLink() ? FOLDER_LINK : FILE; - } - return OTHER; - } } - /** - * Sort {@link Comparator} for use with sorting children and node comparison - */ - static final Comparator DATA_NODE_SORT_COMPARATOR = new DataNodeSortComparator(); + static final Comparator DATA_NODE_COMPARATOR = new DataNodeSortComparator(); - /** - * Search {@link Comparator} for use by {@link #getChild(String, NodeType)} only - */ - private static final DataNodeSearchComparator DATA_NODE_SEARCH_COMPARATOR = - new DataNodeSearchComparator(); + private static boolean useNaturalSort = true; private volatile boolean isCut; // true if this node is marked as cut + public static void setUseNaturalSort(boolean b) { + useNaturalSort = b; + } + @Override public final void setIsCut(boolean isCut) { if (isCut != this.isCut) { @@ -88,6 +80,8 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta return isCut; } + protected abstract NodeType getNodeType(); + /** * Get the project data instance to which this file or folder belongs. * @return project data instance @@ -113,8 +107,9 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta if (!isLoaded()) { return; } + List allChildren = getChildren(); - int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_SORT_COMPARATOR); + int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_COMPARATOR); if (index < 0) { index = -index - 1; } @@ -181,138 +176,199 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta * @param type node type * @return matching tree node or null if not found */ - @SuppressWarnings("unchecked") static GTreeNode getChild(List children, String name, NodeType type) { - ChildSearchRecord childSearchRecord = new ChildSearchRecord(name, type); + + SearchNode key = new SearchNode(name, type); int index = - Collections.binarySearch(children, childSearchRecord, DATA_NODE_SEARCH_COMPARATOR); + Collections.binarySearch(children, key, DATA_NODE_COMPARATOR); return index >= 0 ? children.get(index) : null; } - private record ChildSearchRecord(String name, NodeType type) { - } - - @SuppressWarnings("rawtypes") - private static class DataNodeSearchComparator implements Comparator { - @Override - public int compare(Object o1, Object o2) { - - GTreeNode node = (GTreeNode) o1; - ChildSearchRecord childSearchRecord = (ChildSearchRecord) o2; - - NodeType type1 = NodeType.getNodeType(node); - NodeType type2 = childSearchRecord.type; - - int comp = type1.weight - type2.weight; - if (comp != 0) { - return comp; - } - - // NOTE: This name comparison is consistent with the sort order and - // will provide a case-senstive name-match - comp = compareNodeNames(node.getName(), childSearchRecord.name); - if (comp == 0) { - return type1.ordinal() - type2.ordinal(); - } - return comp; - } - } - - private static class DataNodeSortComparator implements Comparator { - @Override - public int compare(GTreeNode o1, GTreeNode o2) { - - // - // Goal is to have folders appear before files except for folder-links - // which should be grouped with folders but come after a folder with - // the same name - - NodeType type1 = NodeType.getNodeType(o1); - NodeType type2 = NodeType.getNodeType(o2); - - int comp = type1.weight - type2.weight; - if (comp != 0) { - return comp; - } - - // NOTE: This name comparison is consistent with compareTo implementaions - comp = compareNodeNames(o1.getName(), o2.getName()); - if (comp == 0) { - return type1.ordinal() - type2.ordinal(); - } - return comp; - } - } - - /** - * Name comparison to be used for DataTreeNode comparators and node comparison. - * @param n1 first name - * @param n2 second name - * @return comparison result consistent with {@link String#compareTo(String) n1.compareTo(n2)} - */ - static int compareNodeNames(String n1, String n2) { - int c = n1.compareToIgnoreCase(n2); - if (c == 0) { - // disambiguate for deterministic sort - c = n1.compareTo(n2); - } - return c; - } - /** * Generate filtered child nodes for a DomainFolder * @param domainFolder folder * @param filter filter * @param monitor load task monitor - * @return list of filtered chidren + * @return list of filtered children * @throws CancelledException if load task is cancelled */ static List generateChildren(DomainFolder domainFolder, DomainFileFilter filter, TaskMonitor monitor) throws CancelledException { - boolean hideFolderLinks = false; - boolean hideBroken = false; - boolean hideExternal = false; - if (filter != null) { - hideFolderLinks = filter.ignoreFolderLinks(); - hideBroken = filter.ignoreBrokenLinks(); - hideExternal = filter.ignoreExternalLinks(); - } - List children = new ArrayList<>(); - if (domainFolder != null) { - - DomainFolder[] folders = domainFolder.getFolders(); - for (DomainFolder folder : folders) { - monitor.checkCancelled(); - children.add(new DomainFolderNode(folder, filter)); - } - - DomainFile[] files = domainFolder.getFiles(); - for (DomainFile df : files) { - monitor.checkCancelled(); - if (filter != null) { - boolean isFolderLink = df.isLink() && df.getLinkInfo().isFolderLink(); - if (hideFolderLinks && isFolderLink) { - continue; - } - if ((hideBroken || hideExternal) && df.isLink()) { - LinkStatus linkStatus = LinkHandler.getLinkFileStatus(df, null); - if (hideBroken && linkStatus == LinkStatus.BROKEN) { - continue; - } - if (hideExternal && linkStatus == LinkStatus.EXTERNAL) { - continue; - } - } - if (!isFolderLink && !filter.accept(df)) { - continue; - } - } - children.add(new DomainFileNode(df, filter)); - } + if (domainFolder == null) { + return children; } - Collections.sort(children, DATA_NODE_SORT_COMPARATOR); + + DomainFolder[] folders = domainFolder.getFolders(); + for (DomainFolder folder : folders) { + monitor.checkCancelled(); + children.add(new DomainFolderNode(folder, filter)); + } + + DomainFile[] files = domainFolder.getFiles(); + for (DomainFile df : files) { + monitor.checkCancelled(); + if (skip(df, filter)) { + continue; + } + children.add(new DomainFileNode(df, filter)); + } + + Collections.sort(children, DATA_NODE_COMPARATOR); return children; } + + private static boolean skip(DomainFile df, DomainFileFilter filter) { + + if (filter == null) { + return false; + } + + boolean hideFolderLinks = filter.ignoreFolderLinks(); + boolean hideBroken = filter.ignoreBrokenLinks(); + boolean hideExternal = filter.ignoreExternalLinks(); + + boolean isFolderLink = df.isLink() && df.getLinkInfo().isFolderLink(); + if (hideFolderLinks && isFolderLink) { + return true; + } + + if ((hideBroken || hideExternal) && df.isLink()) { + LinkStatus linkStatus = LinkHandler.getLinkFileStatus(df, null); + if (hideBroken && linkStatus == LinkStatus.BROKEN) { + return true; + } + if (hideExternal && linkStatus == LinkStatus.EXTERNAL) { + return true; + } + } + + if (!isFolderLink && !filter.accept(df)) { + return true; + } + + return false; + } + +//================================================================================================= +// Inner Classes +//================================================================================================= + + private static class DataNodeSortComparator implements Comparator { + + private static AlphaNumericComparator alphaNumericComparator = + new AlphaNumericComparator(false); + + @Override + public int compare(GTreeNode o1, GTreeNode o2) { + + // We want folders appear before files except for folder-links which should be grouped + // with folders but come after a folder with the same name + DataTreeNode dtn1 = (DataTreeNode) o1; + DataTreeNode dtn2 = (DataTreeNode) o2; + NodeType type1 = dtn1.getNodeType(); + NodeType type2 = dtn2.getNodeType(); + + int result = type1.weight - type2.weight; + if (result != 0) { + return result; + } + + String n1 = o1.getName(); + String n2 = o2.getName(); + result = compareNames(n1, n2); + if (result == 0) { + return type1.ordinal() - type2.ordinal(); + } + return result; + } + + private int compareNames(String n1, String n2) { + if (useNaturalSort) { + return alphaNumericComparator.compare(n1, n2); + } + + int result = n1.compareToIgnoreCase(n2); + if (result != 0) { + return result; + } + return n1.compareTo(n2); + } + } + + /** + * A dummy search node used to find a child node by the given name and type. + */ + private static class SearchNode extends DataTreeNode { + + private String name; + private NodeType nodeType; + + SearchNode(String name, NodeType nodeType) { + this.name = name; + this.nodeType = nodeType; + } + + @Override + public String getName() { + return name; + } + + @Override + protected NodeType getNodeType() { + return nodeType; + } + + @Override + public ProjectData getProjectData() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPathname() { + throw new UnsupportedOperationException(); + } + + @Override + public int compareTo(GTreeNode node) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException(); + } + + @Override + public GTreeNode getChild(String childName, NodeType type) { + throw new UnsupportedOperationException(); + } + + @Override + public List generateChildren(TaskMonitor monitor) throws CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public Icon getIcon(boolean expanded) { + throw new UnsupportedOperationException(); + } + + @Override + public String getToolTip() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLeaf() { + throw new UnsupportedOperationException(); + } + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java index e8aab8b3ec..389049091c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java @@ -58,7 +58,7 @@ public class DomainFileNode extends DataTreeNode { private volatile String toolTipText; private AtomicInteger refreshCount = new AtomicInteger(); - private DomainFileFilter filter; // relavent when expand folder-link which is a file + private DomainFileFilter filter; // relevant when expand folder-link which is a file private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa"); @@ -114,6 +114,11 @@ public class DomainFileNode extends DataTreeNode { return isFolderLink; } + @Override + protected NodeType getNodeType() { + return isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE; + } + /** * Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}). * @return linked folder or null if this is not a folder-link @@ -403,7 +408,7 @@ public class DomainFileNode extends DataTreeNode { @Override public int compareTo(GTreeNode node) { - return DATA_NODE_SORT_COMPARATOR.compare(this, node); + return DATA_NODE_COMPARATOR.compare(this, node); } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java index 05a662728d..9475104433 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java @@ -20,7 +20,6 @@ import java.util.List; import javax.swing.Icon; -import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; import ghidra.framework.model.*; import ghidra.util.*; @@ -53,13 +52,17 @@ public class DomainFolderNode extends DataTreeNode { this.domainFolder = domainFolder; this.filter = filter; - // TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is if (domainFolder != null) { setToolTipText(); isEditable = domainFolder.isInWritableProject(); } } + @Override + protected NodeType getNodeType() { + return NodeType.FOLDER; + } + @Override public boolean isAutoExpandPermitted() { // Prevent auto-expansion through linked-folders @@ -162,7 +165,7 @@ public class DomainFolderNode extends DataTreeNode { @Override public int compareTo(GTreeNode node) { - return DATA_NODE_SORT_COMPARATOR.compare(this, node); + return DATA_NODE_COMPARATOR.compare(this, node); } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java index a40283cbc8..caf52e6e03 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java @@ -88,7 +88,7 @@ public class ProjectDataTreePanel extends JPanel { /** * Construct an empty data tree panel that is going to be used for the active project tree - * within the frontend tool. + * within the front end tool. * * @param plugin front end plugin */ @@ -126,6 +126,20 @@ public class ProjectDataTreePanel extends JPanel { return tree.getSelectionModel(); } + public void reload() { + if (projectData == null) { + return; + } + + ProjectLocator locator = projectData.getProjectLocator(); + String projectName = locator.getName(); + + GTreeNode oldRoot = root; + root = createRootNode(projectName); + tree.setRootNode(root); + oldRoot.dispose(); + } + /** * Set the project data for this data tree and populate it with * nodes for the users in the project. @@ -528,6 +542,7 @@ public class ProjectDataTreePanel extends JPanel { * Create the root node for this data tree. */ private GTreeNode createRootNode(String projectName) { + if (projectData == null) { return new NoProjectNode(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java index c065e25bde..8dc04de48f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java @@ -143,7 +143,7 @@ public interface Project extends AutoCloseable, Iterable { /** * Allows the user to store data related to the project. - * See {@link #getSaveableData(String)} for future retieval of data. + * See {@link #getSaveableData(String)} for future retrieval of data. * @param key a value used to store and lookup saved data * @param saveState a container of data that will be written out when persisted */ @@ -212,7 +212,7 @@ public interface Project extends AutoCloseable, Iterable { /** * Return a {@link DomainFile} iterator over all non-link files within this project's data store. - * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}. + * If links should be followed use an appropriate static method from {@link ProjectDataUtils}. * @return domain file iterator */ @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java index d3fe7fc423..190b7ffb09 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java @@ -235,7 +235,7 @@ public interface ProjectData extends Iterable { * NOTE: The project should be closed and then reopened after this method is called. * @param newRepository new repository to use * @param force if true any existing local checkout which is not recognized/valid - * for newRepository will be forceably terminated if offline with old repository. + * for newRepository will be forcibly terminated if offline with old repository. * @param monitor task monitor * @throws IOException thrown if files are still checked out, or if there was a problem accessing * the filesystem @@ -281,7 +281,7 @@ public interface ProjectData extends Iterable { /** * Return a {@link DomainFile} iterator over all non-link files within this project data store. - * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}. + * If links should be followed use an appropriate static method from {@link ProjectDataUtils}. * @return domain file iterator */ @Override