mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-25 17:00:36 -09:00
Merge remote-tracking branch
'origin/GP-7144-dragonmacher-front-end-natural-sort--SQUASHED' (Closes #7093)
This commit is contained in:
@@ -464,6 +464,17 @@
|
||||
<TD valign="top" align="left">This controls whether or not Ghidra will compress
|
||||
data being sent to the server.</TD>
|
||||
</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>
|
||||
<TD valign="top" width="200" align="left">Use Notification Animation</TD>
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ import java.util.Comparator;
|
||||
*/
|
||||
public class AlphaNumericComparator implements Comparator<String> {
|
||||
|
||||
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<String> {
|
||||
}
|
||||
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<String> {
|
||||
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()) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import generic.test.AbstractGenericTest;
|
||||
public class AlphaNumericComparatorTest extends AbstractGenericTest {
|
||||
|
||||
@Test
|
||||
public void testIt() {
|
||||
public void testCaseSensitive() {
|
||||
|
||||
List<String> unsorted = Arrays.asList(
|
||||
|
||||
@@ -81,4 +81,61 @@ public class AlphaNumericComparatorTest extends AbstractGenericTest {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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) {
|
||||
projectTab.setBorder(BorderFactory.createTitledBorder(BORDER_PREFIX + projectName));
|
||||
treePanel.updateProjectName(projectName);
|
||||
|
||||
@@ -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<DomainFileInfo, ProjectData> {
|
||||
|
||||
private static boolean useNaturalSort = true;
|
||||
|
||||
private ProjectData projectData;
|
||||
private volatile int modCount;
|
||||
private boolean editingOn;
|
||||
@@ -43,6 +46,10 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
|
||||
super("Project Data Table", serviceProvider);
|
||||
}
|
||||
|
||||
public static void setUseNaturalSort(boolean b) {
|
||||
useNaturalSort = b;
|
||||
}
|
||||
|
||||
boolean loadWasCancelled() {
|
||||
return loadWasCancelled;
|
||||
}
|
||||
@@ -189,6 +196,9 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
|
||||
private class DomainFileNameColumn
|
||||
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
|
||||
|
||||
private static AlphaNumericComparator alphaNumericComparator =
|
||||
new AlphaNumericComparator(false);
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Name";
|
||||
@@ -205,6 +215,14 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<String> getComparator() {
|
||||
if (useNaturalSort) {
|
||||
return alphaNumericComparator;
|
||||
}
|
||||
return super.getComparator();
|
||||
}
|
||||
}
|
||||
|
||||
private class ModificationDateColumn
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
* <ol>
|
||||
* <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
|
||||
* will be placed after the Folder.</li>
|
||||
* </ol>
|
||||
*/
|
||||
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<GTreeNode> DATA_NODE_SORT_COMPARATOR = new DataNodeSortComparator();
|
||||
static final Comparator<GTreeNode> 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<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) {
|
||||
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<GTreeNode> 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<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
|
||||
* @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<GTreeNode> 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<GTreeNode> 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<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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ public interface Project extends AutoCloseable, Iterable<DomainFile> {
|
||||
|
||||
/**
|
||||
* 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<DomainFile> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -235,7 +235,7 @@ public interface ProjectData extends Iterable<DomainFile> {
|
||||
* 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<DomainFile> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
Reference in New Issue
Block a user