Merge remote-tracking branch

'origin/GP-7144-dragonmacher-front-end-natural-sort--SQUASHED'
(Closes #7093)
This commit is contained in:
Ryan Kurtz
2026-08-20 10:48:25 -04:00
14 changed files with 392 additions and 166 deletions

View File

@@ -464,6 +464,17 @@
<TD valign="top" align="left">This controls whether or not Ghidra will compress <TD valign="top" align="left">This controls whether or not Ghidra will compress
data being sent to the server.</TD> data being sent to the server.</TD>
</TR> </TR>
<TR>
<TD valign="top" width="200" align="left">Use Natural Project Sort</TD>
<TD valign="top" align="left">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.
</TD>
</TR>
<TR> <TR>
<TD valign="top" width="200" align="left">Use Notification Animation</TD> <TD valign="top" width="200" align="left">Use Notification Animation</TD>

View File

@@ -23,6 +23,16 @@ import java.util.Comparator;
*/ */
public class AlphaNumericComparator implements Comparator<String> { public class AlphaNumericComparator implements Comparator<String> {
private boolean caseSensitive;
public AlphaNumericComparator() {
this.caseSensitive = true;
}
public AlphaNumericComparator(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
@Override @Override
public int compare(String s1, String s2) { public int compare(String s1, String s2) {
if (s1 == null || s2 == null) { if (s1 == null || s2 == null) {
@@ -66,9 +76,11 @@ public class AlphaNumericComparator implements Comparator<String> {
} }
else { else {
// Compare standard characters alphabetically // Compare standard characters alphabetically
if (c1 != c2) { int result = compare(c1, c2);
return Character.compare(c1, c2); if (result != 0) {
return result;
} }
i++; i++;
j++; j++;
} }
@@ -98,6 +110,19 @@ public class AlphaNumericComparator implements Comparator<String> {
return s1.compareTo(s2); 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) { private long parseNumber(String s, int i) {
long value = 0; long value = 0;
while (i < s.length()) { while (i < s.length()) {

View File

@@ -26,7 +26,7 @@ import generic.test.AbstractGenericTest;
public class AlphaNumericComparatorTest extends AbstractGenericTest { public class AlphaNumericComparatorTest extends AbstractGenericTest {
@Test @Test
public void testIt() { public void testCaseSensitive() {
List<String> unsorted = Arrays.asList( List<String> unsorted = Arrays.asList(
@@ -81,4 +81,61 @@ public class AlphaNumericComparatorTest extends AbstractGenericTest {
assertEquals(sorted, copy); assertEquals(sorted, copy);
} }
@Test
public void testCaseInsensitive() {
List<String> 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<String> 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<String> copy = new ArrayList<>(unsorted);
Collections.sort(copy, new AlphaNumericComparator(false));
assertEquals(sorted, copy);
}
} }

View File

@@ -193,6 +193,10 @@ public class FrontEndPlugin extends Plugin
createToolSpecificOpenActions(); createToolSpecificOpenActions();
} }
void setUseNaturalSort(boolean b) {
projectDataPanel.setUseNaturalSort(b);
}
protected void createToolSpecificOpenActions() { protected void createToolSpecificOpenActions() {
for (DockingAction action : openActions) { for (DockingAction action : openActions) {
tool.removeAction(action); tool.removeAction(action);

View File

@@ -88,15 +88,17 @@ import help.HelpService;
* manner. * manner.
*/ */
public class FrontEndTool extends PluginTool implements OptionsChangeListener { 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"; "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 Boolean ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT = true;
private static final String RESTORE_PREVIOUS_PROJECT_NAME = "Restore Previous Project"; 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, options.registerOption(SHOW_TOOLTIPS_OPTION_NAME, true, help,
"Controls the display of tooltip popup windows."); "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, ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT, help,
"When enabled data buffers sent to Ghidra Server are compressed (see server " + "When enabled data buffers sent to Ghidra Server are compressed (see server " +
"configuration for other direction)"); "configuration for other direction)");
@@ -367,6 +369,9 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
options.registerOption(RESTORE_PREVIOUS_PROJECT_NAME, true, help, options.registerOption(RESTORE_PREVIOUS_PROJECT_NAME, true, help,
"Restore the previous project when Ghidra starts."); "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); defaultLaunchMode = options.getEnum(DEFAULT_TOOL_LAUNCH_MODE, defaultLaunchMode);
boolean autoSave = options.getBoolean(AUTOMATICALLY_SAVE_TOOLS, true); boolean autoSave = options.getBoolean(AUTOMATICALLY_SAVE_TOOLS, true);
@@ -382,7 +387,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
DockingUtils.setGlobalTooltipEnabledOption(showToolTips); DockingUtils.setGlobalTooltipEnabledOption(showToolTips);
boolean compressDataBuffers = boolean compressDataBuffers =
options.getBoolean(ENABLE_COMPRESSED_DATABUFFER_OUTPUT, options.getBoolean(USE_COMPRESSED_DATABUFFER_OUTPUT,
ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT); ENABLE_COMPRESSED_DATABUFFER_OUTPUT_DEFAULT);
DataBuffer.enableCompressedSerializationOutput(compressDataBuffers); DataBuffer.enableCompressedSerializationOutput(compressDataBuffers);
@@ -391,6 +396,9 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
boolean blink = options.getBoolean(BLINKING_CURSORS_OPTION_NAME, true); boolean blink = options.getBoolean(BLINKING_CURSORS_OPTION_NAME, true);
Gui.setBlinkingCursors(blink); Gui.setBlinkingCursors(blink);
boolean useNaturalSort = options.getBoolean(USE_NATURAL_SORT, true);
plugin.setUseNaturalSort(useNaturalSort);
options.addOptionsChangeListener(this); options.addOptionsChangeListener(this);
} }
@@ -412,7 +420,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
else if (SHOW_TOOLTIPS_OPTION_NAME.equals(optionName)) { else if (SHOW_TOOLTIPS_OPTION_NAME.equals(optionName)) {
DockingUtils.setGlobalTooltipEnabledOption((Boolean) newValue); DockingUtils.setGlobalTooltipEnabledOption((Boolean) newValue);
} }
else if (ENABLE_COMPRESSED_DATABUFFER_OUTPUT.equals(optionName)) { else if (USE_COMPRESSED_DATABUFFER_OUTPUT.equals(optionName)) {
DataBuffer.enableCompressedSerializationOutput((Boolean) newValue); DataBuffer.enableCompressedSerializationOutput((Boolean) newValue);
} }
else if (RESTORE_PREVIOUS_PROJECT_NAME.equals(optionName)) { 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)) { else if (BLINKING_CURSORS_OPTION_NAME.equals(optionName)) {
Gui.setBlinkingCursors((Boolean) newValue); Gui.setBlinkingCursors((Boolean) newValue);
} }
else if (USE_NATURAL_SORT.equals(optionName)) {
plugin.setUseNaturalSort((Boolean) newValue);
}
} }
@Override @Override

View File

@@ -19,15 +19,17 @@ import java.awt.BorderLayout;
import java.awt.Component; import java.awt.Component;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.*;
import java.util.Map; import java.util.Map.Entry;
import javax.swing.*; import javax.swing.*;
import docking.*; import docking.*;
import docking.widgets.tabbedpane.DockingTabRenderer; import docking.widgets.tabbedpane.DockingTabRenderer;
import ghidra.framework.client.NotConnectedException; import ghidra.framework.client.NotConnectedException;
import ghidra.framework.main.datatable.ProjectDataTableModel;
import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.DataTreeNode;
import ghidra.framework.main.datatree.ProjectDataTreePanel; import ghidra.framework.main.datatree.ProjectDataTreePanel;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.SaveState; import ghidra.framework.options.SaveState;
@@ -212,7 +214,7 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
} }
try { 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); ProjectData projectData = activeProject.addProjectView(projectView, true);
if (projectData == null) { if (projectData == null) {
return null; // repository connection may have been cancelled 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<Entry<ProjectLocator, ProjectDataTreePanel>> entries = readOnlyViews.entrySet();
for (Entry<ProjectLocator, ProjectDataTreePanel> entry : entries) {
ProjectDataTreePanel pdtp = entry.getValue();
if (pdtp != null) {
pdtp.reload();
}
}
}
void setBorder(String projectName) { void setBorder(String projectName) {
projectTab.setBorder(BorderFactory.createTitledBorder(BORDER_PREFIX + projectName)); projectTab.setBorder(BorderFactory.createTitledBorder(BORDER_PREFIX + projectName));
treePanel.updateProjectName(projectName); treePanel.updateProjectName(projectName);

View File

@@ -27,12 +27,15 @@ import ghidra.util.InvalidNameException;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.datastruct.Accumulator; import ghidra.util.datastruct.Accumulator;
import ghidra.util.datastruct.AlphaNumericComparator;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException; import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, ProjectData> { public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, ProjectData> {
private static boolean useNaturalSort = true;
private ProjectData projectData; private ProjectData projectData;
private volatile int modCount; private volatile int modCount;
private boolean editingOn; private boolean editingOn;
@@ -43,6 +46,10 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
super("Project Data Table", serviceProvider); super("Project Data Table", serviceProvider);
} }
public static void setUseNaturalSort(boolean b) {
useNaturalSort = b;
}
boolean loadWasCancelled() { boolean loadWasCancelled() {
return loadWasCancelled; return loadWasCancelled;
} }
@@ -189,6 +196,9 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
private class DomainFileNameColumn private class DomainFileNameColumn
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> { extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
private static AlphaNumericComparator alphaNumericComparator =
new AlphaNumericComparator(false);
@Override @Override
public String getColumnName() { public String getColumnName() {
return "Name"; return "Name";
@@ -205,6 +215,14 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
public int getColumnPreferredWidth() { public int getColumnPreferredWidth() {
return 200; return 200;
} }
@Override
public Comparator<String> getComparator() {
if (useNaturalSort) {
return alphaNumericComparator;
}
return super.getComparator();
}
} }
private class ModificationDateColumn private class ModificationDateColumn

View File

@@ -255,14 +255,13 @@ public class ProjectDataTablePanel extends JPanel {
} }
} }
private void reload() { public void reload() {
checkCapacity(); checkCapacity();
if (!capacityExceeded) { if (!capacityExceeded) {
model.reload(); model.reload();
} }
} }
// load the max file count system property // load the max file count system property

View File

@@ -17,11 +17,14 @@ package ghidra.framework.main.datatree;
import java.util.*; import java.util.*;
import javax.swing.Icon;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.GTreeSlowLoadingNode; import docking.widgets.tree.GTreeSlowLoadingNode;
import ghidra.framework.data.LinkHandler; import ghidra.framework.data.LinkHandler;
import ghidra.framework.data.LinkHandler.LinkStatus; import ghidra.framework.data.LinkHandler.LinkStatus;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.util.datastruct.AlphaNumericComparator;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; 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: * sort order is based upon the following comparisons in order of significance:
* <ol> * <ol>
* <li>Node type weighting. Folder and Folder-Links have equal weighting.</li> * <li>Node type weighting. Folder and Folder-Links have equal weighting.</li>
* <li>Node comparison by name (see {@link DataTreeNode#compareNodeNames(String, String)}).</li> * <li>Node comparison by name.</li>
* <li>Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder * <li>Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder
* will be placed after the Folder.</li> * will be placed after the Folder.</li>
* </ol> * </ol>
*/ */
enum NodeType { 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) { NodeType(int weight) {
this.weight = 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;
}
} }
/** static final Comparator<GTreeNode> DATA_NODE_COMPARATOR = new DataNodeSortComparator();
* Sort {@link Comparator} for use with sorting children and node comparison
*/
static final Comparator<GTreeNode> DATA_NODE_SORT_COMPARATOR = new DataNodeSortComparator();
/** private static boolean useNaturalSort = true;
* Search {@link Comparator} for use by {@link #getChild(String, NodeType)} only
*/
private static final DataNodeSearchComparator DATA_NODE_SEARCH_COMPARATOR =
new DataNodeSearchComparator();
private volatile boolean isCut; // true if this node is marked as cut private volatile boolean isCut; // true if this node is marked as cut
public static void setUseNaturalSort(boolean b) {
useNaturalSort = b;
}
@Override @Override
public final void setIsCut(boolean isCut) { public final void setIsCut(boolean isCut) {
if (isCut != this.isCut) { if (isCut != this.isCut) {
@@ -88,6 +80,8 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
return isCut; return isCut;
} }
protected abstract NodeType getNodeType();
/** /**
* Get the project data instance to which this file or folder belongs. * Get the project data instance to which this file or folder belongs.
* @return project data instance * @return project data instance
@@ -113,8 +107,9 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
if (!isLoaded()) { if (!isLoaded()) {
return; return;
} }
List<GTreeNode> allChildren = getChildren(); List<GTreeNode> allChildren = getChildren();
int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_SORT_COMPARATOR); int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_COMPARATOR);
if (index < 0) { if (index < 0) {
index = -index - 1; index = -index - 1;
} }
@@ -181,138 +176,199 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
* @param type node type * @param type node type
* @return matching tree node or null if not found * @return matching tree node or null if not found
*/ */
@SuppressWarnings("unchecked")
static GTreeNode getChild(List<GTreeNode> children, String name, NodeType type) { static GTreeNode getChild(List<GTreeNode> children, String name, NodeType type) {
ChildSearchRecord childSearchRecord = new ChildSearchRecord(name, type);
SearchNode key = new SearchNode(name, type);
int index = int index =
Collections.binarySearch(children, childSearchRecord, DATA_NODE_SEARCH_COMPARATOR); Collections.binarySearch(children, key, DATA_NODE_COMPARATOR);
return index >= 0 ? children.get(index) : null; 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<GTreeNode> {
@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 * Generate filtered child nodes for a DomainFolder
* @param domainFolder folder * @param domainFolder folder
* @param filter filter * @param filter filter
* @param monitor load task monitor * @param monitor load task monitor
* @return list of filtered chidren * @return list of filtered children
* @throws CancelledException if load task is cancelled * @throws CancelledException if load task is cancelled
*/ */
static List<GTreeNode> generateChildren(DomainFolder domainFolder, DomainFileFilter filter, static List<GTreeNode> generateChildren(DomainFolder domainFolder, DomainFileFilter filter,
TaskMonitor monitor) throws CancelledException { 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<GTreeNode> children = new ArrayList<>(); List<GTreeNode> children = new ArrayList<>();
if (domainFolder != null) { if (domainFolder == null) {
return children;
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));
}
} }
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; 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<GTreeNode> {
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<GTreeNode> 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();
}
}
} }

View File

@@ -58,7 +58,7 @@ public class DomainFileNode extends DataTreeNode {
private volatile String toolTipText; private volatile String toolTipText;
private AtomicInteger refreshCount = new AtomicInteger(); 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"); private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
@@ -114,6 +114,11 @@ public class DomainFileNode extends DataTreeNode {
return isFolderLink; 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()}). * Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}).
* @return linked folder or null if this is not a folder-link * @return linked folder or null if this is not a folder-link
@@ -403,7 +408,7 @@ public class DomainFileNode extends DataTreeNode {
@Override @Override
public int compareTo(GTreeNode node) { public int compareTo(GTreeNode node) {
return DATA_NODE_SORT_COMPARATOR.compare(this, node); return DATA_NODE_COMPARATOR.compare(this, node);
} }
@Override @Override

View File

@@ -20,7 +20,6 @@ import java.util.List;
import javax.swing.Icon; import javax.swing.Icon;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.util.*; import ghidra.util.*;
@@ -53,13 +52,17 @@ public class DomainFolderNode extends DataTreeNode {
this.domainFolder = domainFolder; this.domainFolder = domainFolder;
this.filter = filter; 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) { if (domainFolder != null) {
setToolTipText(); setToolTipText();
isEditable = domainFolder.isInWritableProject(); isEditable = domainFolder.isInWritableProject();
} }
} }
@Override
protected NodeType getNodeType() {
return NodeType.FOLDER;
}
@Override @Override
public boolean isAutoExpandPermitted() { public boolean isAutoExpandPermitted() {
// Prevent auto-expansion through linked-folders // Prevent auto-expansion through linked-folders
@@ -162,7 +165,7 @@ public class DomainFolderNode extends DataTreeNode {
@Override @Override
public int compareTo(GTreeNode node) { public int compareTo(GTreeNode node) {
return DATA_NODE_SORT_COMPARATOR.compare(this, node); return DATA_NODE_COMPARATOR.compare(this, node);
} }
@Override @Override

View File

@@ -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 * 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 * @param plugin front end plugin
*/ */
@@ -126,6 +126,20 @@ public class ProjectDataTreePanel extends JPanel {
return tree.getSelectionModel(); 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 * Set the project data for this data tree and populate it with
* nodes for the users in the project. * nodes for the users in the project.
@@ -528,6 +542,7 @@ public class ProjectDataTreePanel extends JPanel {
* Create the root node for this data tree. * Create the root node for this data tree.
*/ */
private GTreeNode createRootNode(String projectName) { private GTreeNode createRootNode(String projectName) {
if (projectData == null) { if (projectData == null) {
return new NoProjectNode(); return new NoProjectNode();
} }

View File

@@ -143,7 +143,7 @@ public interface Project extends AutoCloseable, Iterable<DomainFile> {
/** /**
* Allows the user to store data related to the project. * 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 key a value used to store and lookup saved data
* @param saveState a container of data that will be written out when persisted * @param saveState a container of data that will be written out when persisted
*/ */
@@ -212,7 +212,7 @@ public interface Project extends AutoCloseable, Iterable<DomainFile> {
/** /**
* Return a {@link DomainFile} iterator over all non-link files within this project's data store. * 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 * @return domain file iterator
*/ */
@Override @Override

View File

@@ -235,7 +235,7 @@ public interface ProjectData extends Iterable<DomainFile> {
* NOTE: The project should be closed and then reopened after this method is called. * NOTE: The project should be closed and then reopened after this method is called.
* @param newRepository new repository to use * @param newRepository new repository to use
* @param force if true any existing local checkout which is not recognized/valid * @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 * @param monitor task monitor
* @throws IOException thrown if files are still checked out, or if there was a problem accessing * @throws IOException thrown if files are still checked out, or if there was a problem accessing
* the filesystem * the filesystem
@@ -281,7 +281,7 @@ public interface ProjectData extends Iterable<DomainFile> {
/** /**
* Return a {@link DomainFile} iterator over all non-link files within this project data store. * 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 * @return domain file iterator
*/ */
@Override @Override