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:
*
* - Node type weighting. Folder and Folder-Links have equal weighting.
- * - Node comparison by name (see {@link DataTreeNode#compareNodeNames(String, String)}).
+ * - Node comparison by name.
* - Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder
* will be placed after the Folder.
*
*/
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