mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
Fixed open program dialog to not repeatedly load the root node
This commit is contained in:
parent
2eff37f655
commit
1d5da6dae1
9 changed files with 268 additions and 165 deletions
|
@ -32,16 +32,19 @@ import docking.event.mouse.GMouseListenerAdapter;
|
||||||
import docking.widgets.combobox.GComboBox;
|
import docking.widgets.combobox.GComboBox;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
|
import docking.widgets.tree.GTree;
|
||||||
|
import docking.widgets.tree.GTreeTask;
|
||||||
import docking.widgets.tree.support.GTreeSelectionEvent;
|
import docking.widgets.tree.support.GTreeSelectionEvent;
|
||||||
import docking.widgets.tree.support.GTreeSelectionListener;
|
import docking.widgets.tree.support.GTreeSelectionListener;
|
||||||
import ghidra.framework.main.datatree.DialogProjectTreeContext;
|
import ghidra.framework.main.datatree.*;
|
||||||
import ghidra.framework.main.datatree.ProjectDataTreePanel;
|
|
||||||
import ghidra.framework.main.projectdata.actions.*;
|
import ghidra.framework.main.projectdata.actions.*;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.layout.PairLayout;
|
import ghidra.util.layout.PairLayout;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base dialog for choosing DomainFiles. Provides and manages the base data tree panel. Subclasses
|
* Base dialog for choosing DomainFiles. Provides and manages the base data tree panel. Subclasses
|
||||||
|
@ -68,7 +71,6 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
private DataTreeDialogType type;
|
private DataTreeDialogType type;
|
||||||
private Component parent;
|
private Component parent;
|
||||||
|
|
||||||
private String searchString;
|
|
||||||
private boolean cancelled = false;
|
private boolean cancelled = false;
|
||||||
|
|
||||||
private ProjectDataExpandAction<DialogProjectTreeContext> expandAction;
|
private ProjectDataExpandAction<DialogProjectTreeContext> expandAction;
|
||||||
|
@ -183,13 +185,20 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GTree getTree() {
|
||||||
|
return treePanel.getDataTree();
|
||||||
|
}
|
||||||
|
|
||||||
public String getNameText() {
|
public String getNameText() {
|
||||||
return nameField.getText();
|
return nameField.getText();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setNameText(String name) {
|
public void setNameText(String name) {
|
||||||
nameField.setText(name.trim());
|
// We need to run this code in a task since the tree may already be processing other tasks
|
||||||
nameField.selectAll();
|
// that would override this setting when they are run. But putting this task in the queue,
|
||||||
|
// we get the correct UI update ordering.
|
||||||
|
DataTree tree = treePanel.getDataTree();
|
||||||
|
tree.runTask(new SetNameTextTask(tree, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -327,13 +336,6 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the root folder in the tree.
|
|
||||||
*/
|
|
||||||
public void selectRootDataFolder() {
|
|
||||||
Swing.runLater(() -> treePanel.selectRootDataFolder());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a folder in the tree.
|
* Select a folder in the tree.
|
||||||
* @param folder the folder to select
|
* @param folder the folder to select
|
||||||
|
@ -369,13 +371,14 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
|
|
||||||
// data tree panel must be created before the combo box
|
// data tree panel must be created before the combo box
|
||||||
JPanel dataTreePanel = createDataTreePanel();
|
JPanel dataTreePanel = createDataTreePanel();
|
||||||
ProjectData pd = project.getProjectData();
|
|
||||||
treePanel.setProjectData(project.getName(), pd);
|
if (type == CHOOSE_FOLDER) {
|
||||||
|
// this allows users to press the OK button to choose the root folder
|
||||||
treePanel.selectRootDataFolder();
|
treePanel.selectRootDataFolder();
|
||||||
|
}
|
||||||
|
|
||||||
if (type == OPEN) {
|
if (type == OPEN) {
|
||||||
JPanel comboPanel = createComboBoxPanel();
|
JPanel comboPanel = createComboBoxPanel();
|
||||||
|
|
||||||
panel.add(comboPanel, BorderLayout.NORTH);
|
panel.add(comboPanel, BorderLayout.NORTH);
|
||||||
populateProjectModel();
|
populateProjectModel();
|
||||||
}
|
}
|
||||||
|
@ -578,7 +581,7 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSearchText(String s) {
|
public void setSearchText(String s) {
|
||||||
if (searchString != null) {
|
if (s != null) {
|
||||||
treePanel.findAndSelect(s);
|
treePanel.findAndSelect(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,4 +607,21 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SetNameTextTask extends GTreeTask {
|
||||||
|
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
SetNameTextTask(GTree gTree, String text) {
|
||||||
|
super(gTree);
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
runOnSwingThread(() -> {
|
||||||
|
nameField.setText(text.trim());
|
||||||
|
nameField.selectAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedI
|
||||||
runSwing(() -> {
|
runSwing(() -> {
|
||||||
dataTreeDialog.selectDomainFile(file);
|
dataTreeDialog.selectDomainFile(file);
|
||||||
});
|
});
|
||||||
|
waitForTree(dataTreeDialog.getTree());
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
pressButtonByText(dataTreeDialog, "OK");
|
pressButtonByText(dataTreeDialog, "OK");
|
||||||
|
|
||||||
|
@ -131,6 +132,7 @@ public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedI
|
||||||
runSwing(() -> {
|
runSwing(() -> {
|
||||||
dataTreeDialog.selectFolder(folder);
|
dataTreeDialog.selectFolder(folder);
|
||||||
});
|
});
|
||||||
|
waitForTree(dataTreeDialog.getTree());
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
pressButtonByText(dataTreeDialog, "OK");
|
pressButtonByText(dataTreeDialog, "OK");
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import ghidra.program.database.ProgramBuilder;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
import ghidra.test.TestEnv;
|
import ghidra.test.TestEnv;
|
||||||
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
@ -45,15 +46,6 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
private DataTreeDialog dialog;
|
private DataTreeDialog dialog;
|
||||||
private List<String> names =
|
private List<String> names =
|
||||||
List.of("notepad", "XNotepad", "tNotepadA", "tNotepadB", "tNotepadC", "tNotepadD");
|
List.of("notepad", "XNotepad", "tNotepadA", "tNotepadB", "tNotepadC", "tNotepadD");
|
||||||
private GTree gtree;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for DataTreeDialogTest.
|
|
||||||
* @param arg0
|
|
||||||
*/
|
|
||||||
public DataTreeDialogTest() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
@ -85,6 +77,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
|
closeAllWindows();
|
||||||
env.dispose();
|
env.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +165,14 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOKButtonAlwaysEnabled_Type_CHOOSE_FOLDER() {
|
public void testOKButtonAlwaysEnabled_Type_CHOOSE_FOLDER() {
|
||||||
// no initial selection--button disabled
|
//
|
||||||
|
// This tests that when choosing a folder, the root is selected by default so that users can
|
||||||
|
// simply press OK to pick a folder quickly.
|
||||||
|
//
|
||||||
|
// **Also, the tree remembers folder selections and files selections, with the file's parent
|
||||||
|
// being the selected folder. It appears that you cannot get rid of the dialog's notion of
|
||||||
|
// the current folder. This tests that.
|
||||||
|
//
|
||||||
show(CHOOSE_FOLDER);
|
show(CHOOSE_FOLDER);
|
||||||
assertOK(true);
|
assertOK(true);
|
||||||
|
|
||||||
|
@ -253,28 +253,17 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
List.of("/dir1/dir2/file1", "/dir1/dir2a/dir3a/file2", "/file3"));
|
List.of("/dir1/dir2/file1", "/dir1/dir2a/dir3a/file2", "/file3"));
|
||||||
show(OPEN);
|
show(OPEN);
|
||||||
|
|
||||||
Set<DomainFile> selectedProjectElements = new HashSet<>();
|
|
||||||
ProjectDataTreePanel projectDataTreePanel = getProjectDataTreePanel();
|
ProjectDataTreePanel projectDataTreePanel = getProjectDataTreePanel();
|
||||||
projectDataTreePanel.addTreeSelectionListener(
|
|
||||||
e -> {
|
|
||||||
for (TreePath treePath : e.getPaths()) {
|
|
||||||
Object leafNode = treePath.getLastPathComponent();
|
|
||||||
if (leafNode instanceof DomainFileNode) {
|
|
||||||
selectedProjectElements.add(((DomainFileNode) leafNode).getDomainFile());
|
|
||||||
}
|
|
||||||
// else if (leafNode instanceof DomainFolderNode) {
|
|
||||||
// selectedProjectElements
|
|
||||||
// .add(((DomainFolderNode) leafNode).getDomainFolder());
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
projectDataTreePanel.selectDomainFiles(Set.of(createdFiles.get(0), createdFiles.get(1)));
|
DomainFile file1 = createdFiles.get(0);
|
||||||
waitForSwing();
|
DomainFile file2 = createdFiles.get(1);
|
||||||
|
projectDataTreePanel.selectDomainFiles(Set.of(file1, file2));
|
||||||
|
waitForTree(getGTree());
|
||||||
|
|
||||||
assertEquals(selectedProjectElements.size(), 2);
|
Set<DomainFile> selectedData = getSelectedFiles();
|
||||||
assertTrue(selectedProjectElements.contains(createdFiles.get(0)));
|
assertEquals(selectedData.size(), 2);
|
||||||
assertTrue(selectedProjectElements.contains(createdFiles.get(1)));
|
assertTrue(selectedData.contains(file1));
|
||||||
|
assertTrue(selectedData.contains(file2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -283,33 +272,46 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
List.of("/dir1/dir2/file1", "/dir1/dir2a/dir3a/file2", "/file3"));
|
List.of("/dir1/dir2/file1", "/dir1/dir2a/dir3a/file2", "/file3"));
|
||||||
show(OPEN);
|
show(OPEN);
|
||||||
|
|
||||||
Set<DomainFolder> selectedProjectElements = new HashSet<>();
|
|
||||||
ProjectDataTreePanel projectDataTreePanel = getProjectDataTreePanel();
|
ProjectDataTreePanel projectDataTreePanel = getProjectDataTreePanel();
|
||||||
projectDataTreePanel.addTreeSelectionListener(
|
|
||||||
e -> {
|
|
||||||
for (TreePath treePath : e.getPaths()) {
|
|
||||||
Object leafNode = treePath.getLastPathComponent();
|
|
||||||
// if (leafNode instanceof DomainFileNode) {
|
|
||||||
// selectedProjectElements.add(((DomainFileNode) leafNode).getDomainFile());
|
|
||||||
// }
|
|
||||||
if (leafNode instanceof DomainFolderNode) {
|
|
||||||
selectedProjectElements
|
|
||||||
.add(((DomainFolderNode) leafNode).getDomainFolder());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
projectDataTreePanel.selectDomainFolder(createdFiles.get(0).getParent());
|
DomainFile file1 = createdFiles.get(0);
|
||||||
|
DomainFolder dir2 = file1.getParent();
|
||||||
|
projectDataTreePanel.selectDomainFolder(dir2);
|
||||||
waitForTree(getGTree());
|
waitForTree(getGTree());
|
||||||
waitForSwing();
|
|
||||||
|
|
||||||
assertEquals(selectedProjectElements.size(), 1);
|
Set<DomainFolder> selectedData = getSelectedFolders();
|
||||||
assertTrue(selectedProjectElements.contains(createdFiles.get(0).getParent()));
|
assertEquals(selectedData.size(), 1);
|
||||||
|
assertTrue("'dir2' not selected", selectedData.contains(dir2));
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
// Private
|
// Private
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
||||||
|
private Set<DomainFolder> getSelectedFolders() {
|
||||||
|
Set<DomainFolder> set = new HashSet<>();
|
||||||
|
TreePath[] paths = getGTree().getSelectionPaths();
|
||||||
|
for (TreePath path : paths) {
|
||||||
|
Object leafNode = path.getLastPathComponent();
|
||||||
|
if (leafNode instanceof DomainFolderNode folderNode) {
|
||||||
|
set.add(folderNode.getDomainFolder());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<DomainFile> getSelectedFiles() {
|
||||||
|
Set<DomainFile> set = new HashSet<>();
|
||||||
|
TreePath[] paths = getGTree().getSelectionPaths();
|
||||||
|
for (TreePath path : paths) {
|
||||||
|
Object leafNode = path.getLastPathComponent();
|
||||||
|
if (leafNode instanceof DomainFileNode fileNode) {
|
||||||
|
set.add(fileNode.getDomainFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
private void deselectFolder() {
|
private void deselectFolder() {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
@ -400,9 +402,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void show(DataTreeDialogType type) {
|
private void show(DataTreeDialogType type) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
Swing.runLater(() -> {
|
||||||
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", type);
|
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", type);
|
||||||
|
|
||||||
dialog.showComponent();
|
dialog.showComponent();
|
||||||
});
|
});
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
|
@ -410,9 +411,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void show(DataTreeDialogType type, final String name) {
|
private void show(DataTreeDialogType type, final String name) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
Swing.runLater(() -> {
|
||||||
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", type);
|
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", type);
|
||||||
|
|
||||||
dialog.setNameText(name);
|
dialog.setNameText(name);
|
||||||
dialog.showComponent();
|
dialog.showComponent();
|
||||||
});
|
});
|
||||||
|
@ -423,7 +423,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFiltered(final String startsWith) {
|
private void showFiltered(final String startsWith) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
Swing.runLater(() -> {
|
||||||
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
|
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
|
||||||
OPEN, f -> f.getName().startsWith(startsWith));
|
OPEN, f -> f.getName().startsWith(startsWith));
|
||||||
dialog.showComponent();
|
dialog.showComponent();
|
||||||
|
|
|
@ -57,8 +57,14 @@ import resources.Icons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for creating a JTree that supports filtering, threading, and a progress bar.
|
* Class for creating a JTree that supports filtering, threading, and a progress bar.
|
||||||
|
* <p>
|
||||||
|
* Note: when calling methods on this class to select nodes, if those nodes are threaded, or extend
|
||||||
|
* from {@link GTreeSlowLoadingNode}, then you must first expand the paths you wish to select. You
|
||||||
|
* can do this by calling {@link #expandAndSelectPaths(List)}. The various select methods of this
|
||||||
|
* class will not expand nodes, but they will trigger children to be loaded. If those nodes are not
|
||||||
|
* threaded, then the tree will add and expand the children by default. When using threaded nodes,
|
||||||
|
* the delay in loading prevents the tree from correctly expanding the paths.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class GTree extends JPanel implements BusyListener {
|
public class GTree extends JPanel implements BusyListener {
|
||||||
private static final Color BACKGROUND = new GColor("color.bg.tree");
|
private static final Color BACKGROUND = new GColor("color.bg.tree");
|
||||||
private AutoScrollTree tree;
|
private AutoScrollTree tree;
|
||||||
|
@ -475,26 +481,36 @@ public class GTree extends JPanel implements BusyListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandPath(GTreeNode node) {
|
public void expandPath(GTreeNode node) {
|
||||||
expandPaths(new TreePath[] { node.getTreePath() });
|
expandPaths(List.of(node.getTreePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandPath(TreePath path) {
|
public void expandPath(TreePath path) {
|
||||||
expandPaths(new TreePath[] { path });
|
expandPaths(List.of(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandPaths(TreePath[] paths) {
|
public void expandPaths(TreePath[] paths) {
|
||||||
runTask(new GTreeExpandPathsTask(this, Arrays.asList(paths)));
|
expandPaths(Arrays.asList(paths));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandPaths(List<TreePath> pathsList) {
|
public void expandPaths(List<TreePath> paths) {
|
||||||
TreePath[] treePaths = pathsList.toArray(new TreePath[pathsList.size()]);
|
runTask(new GTreeExpandPathsTask(this, paths));
|
||||||
expandPaths(treePaths);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearSelectionPaths() {
|
public void clearSelectionPaths() {
|
||||||
runTask(new GTreeClearSelectionTask(this, tree));
|
runTask(new GTreeClearSelectionTask(this, tree));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands and then selects the given paths. You must use this method if your tree is using
|
||||||
|
* {@link GTreeSlowLoadingNode}s. Otherwise, if the given paths are not expanded, then the
|
||||||
|
* select will not work. More info at the class javadoc.
|
||||||
|
*
|
||||||
|
* @param paths the paths
|
||||||
|
*/
|
||||||
|
public void expandAndSelectPaths(List<TreePath> paths) {
|
||||||
|
setSelectionPaths(paths, true, EventOrigin.API_GENERATED);
|
||||||
|
}
|
||||||
|
|
||||||
public void setSelectedNode(GTreeNode node) {
|
public void setSelectedNode(GTreeNode node) {
|
||||||
setSelectionPaths(new TreePath[] { node.getTreePath() });
|
setSelectionPaths(new TreePath[] { node.getTreePath() });
|
||||||
}
|
}
|
||||||
|
@ -568,8 +584,24 @@ public class GTree extends JPanel implements BusyListener {
|
||||||
runTask(new GTreeSelectNodeByNameTask(this, tree, namePath, EventOrigin.API_GENERATED));
|
runTask(new GTreeSelectNodeByNameTask(this, tree, namePath, EventOrigin.API_GENERATED));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectionPaths(TreePath[] path, EventOrigin origin) {
|
public void setSelectionPaths(TreePath[] paths, EventOrigin origin) {
|
||||||
runTask(new GTreeSelectPathsTask(this, tree, Arrays.asList(path), origin));
|
setSelectionPaths(Arrays.asList(paths), false, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the given paths, expanding them first if requested.
|
||||||
|
*
|
||||||
|
* @param paths the paths to select
|
||||||
|
* @param expandPaths true to expand the paths first; this is only needed for multi-threaded
|
||||||
|
* nodes. Non-threaded nodes should use false, as it increase performance.
|
||||||
|
* @param origin the event type; use {@link EventOrigin#API_GENERATED} if unsure
|
||||||
|
*/
|
||||||
|
public void setSelectionPaths(List<TreePath> paths, boolean expandPaths, EventOrigin origin) {
|
||||||
|
|
||||||
|
if (expandPaths) {
|
||||||
|
expandPaths(paths);
|
||||||
|
}
|
||||||
|
runTask(new GTreeSelectPathsTask(this, tree, paths, origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCollapsed(TreePath path) {
|
public boolean isCollapsed(TreePath path) {
|
||||||
|
|
|
@ -15,10 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package docking.widgets.tree;
|
package docking.widgets.tree;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
|
import org.apache.commons.collections4.IteratorUtils;
|
||||||
|
|
||||||
import docking.widgets.tree.internal.InProgressGTreeNode;
|
import docking.widgets.tree.internal.InProgressGTreeNode;
|
||||||
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
@ -124,4 +126,14 @@ public abstract class GTreeSlowLoadingNode extends GTreeLazyNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<GTreeNode> iterator(boolean depthFirst) {
|
||||||
|
if (Swing.isSwingThread()) {
|
||||||
|
Msg.warn(this, "Threaded tree nodes cannot be iterated on the Swing thread. " +
|
||||||
|
"Change the call to this method to be run in a background task");
|
||||||
|
return IteratorUtils.emptyIterator();
|
||||||
|
}
|
||||||
|
return super.iterator(depthFirst);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,7 @@ package docking.widgets.tree;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
|
@ -151,7 +150,27 @@ public class GTreeSlowLoadingNodeTest extends AbstractDockingTest {
|
||||||
waitForTree();
|
waitForTree();
|
||||||
Swing.runNow(() -> children = nonLeaf1.getChildren());
|
Swing.runNow(() -> children = nonLeaf1.getChildren());
|
||||||
assertTrue("Did not find children for: " + nonLeaf1, nonLeaf1.getChildCount() > 1);
|
assertTrue("Did not find children for: " + nonLeaf1, nonLeaf1.getChildCount() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIterator() {
|
||||||
|
|
||||||
|
gTree.setRootNode(new TestRootNode(5000));
|
||||||
|
waitForTree();
|
||||||
|
|
||||||
|
GTreeNode rootNode = gTree.getModelRoot();
|
||||||
|
GTreeNode slowChild = rootNode.getChild(0); // slow; threaded
|
||||||
|
assertNotNull(slowChild);
|
||||||
|
|
||||||
|
Swing.runNow(() -> children = slowChild.getChildren());
|
||||||
|
assertEquals(1, children.size());
|
||||||
|
assertTrue(children.get(0) instanceof InProgressGTreeNode);
|
||||||
|
|
||||||
|
Iterator<GTreeNode> it = runSwing(() -> slowChild.iterator(true));
|
||||||
|
assertFalse(it.hasNext()); // empty when called on the Swing thread
|
||||||
|
|
||||||
|
it = slowChild.iterator(true);
|
||||||
|
assertTrue(it.hasNext()); // not empty on non-Swing thread
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
@ -235,11 +254,11 @@ public class GTreeSlowLoadingNodeTest extends AbstractDockingTest {
|
||||||
private class TestRootNode extends GTreeNode {
|
private class TestRootNode extends GTreeNode {
|
||||||
|
|
||||||
TestRootNode(int loadDelayMillis) {
|
TestRootNode(int loadDelayMillis) {
|
||||||
List<GTreeNode> children = new ArrayList<>();
|
List<GTreeNode> newChildren = new ArrayList<>();
|
||||||
children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
|
newChildren.add(new TestSlowLoadingNode(loadDelayMillis, 1));
|
||||||
children.add(new TestLeafNode());
|
newChildren.add(new TestLeafNode());
|
||||||
children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
|
newChildren.add(new TestSlowLoadingNode(loadDelayMillis, 1));
|
||||||
setChildren(children);
|
setChildren(newChildren);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -289,18 +308,18 @@ public class GTreeSlowLoadingNodeTest extends AbstractDockingTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
int childCount = getRandomInt(MIN_CHILD_COUNT, MAX_CHILD_COUNT);
|
int childCount = getRandomInt(MIN_CHILD_COUNT, MAX_CHILD_COUNT);
|
||||||
List<GTreeNode> children = new ArrayList<>();
|
List<GTreeNode> newChildren = new ArrayList<>();
|
||||||
for (int i = 0; i < childCount; i++) {
|
for (int i = 0; i < childCount; i++) {
|
||||||
monitor.checkCancelled();
|
monitor.checkCancelled();
|
||||||
int value = getRandomInt(0, 1);
|
int value = getRandomInt(0, 1);
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
children.add(new TestSlowLoadingNode(loadDelayMillis, depth + 1));
|
newChildren.add(new TestSlowLoadingNode(loadDelayMillis, depth + 1));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
children.add(new TestLeafNode());
|
newChildren.add(new TestLeafNode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return children;
|
return newChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -318,7 +318,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
r.run();
|
r.run();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SwingUtilities.invokeLater(r);
|
Swing.runLater(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,12 +415,12 @@ public class FrontEndPlugin extends Plugin
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void viewedProjectAdded(URL projectView) {
|
public void viewedProjectAdded(URL projectView) {
|
||||||
SwingUtilities.invokeLater(() -> rebuildRecentMenus());
|
Swing.runLater(() -> rebuildRecentMenus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void viewedProjectRemoved(URL projectView) {
|
public void viewedProjectRemoved(URL projectView) {
|
||||||
SwingUtilities.invokeLater(() -> rebuildRecentMenus());
|
Swing.runLater(() -> rebuildRecentMenus());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -536,7 +536,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
void selectFiles(final Set<DomainFile> files) {
|
void selectFiles(final Set<DomainFile> files) {
|
||||||
// Do this later in case any of the given files are newly created, which means that the
|
// Do this later in case any of the given files are newly created, which means that the
|
||||||
// GUIs may have not yet been notified.
|
// GUIs may have not yet been notified.
|
||||||
SwingUtilities.invokeLater(() -> {
|
Swing.runLater(() -> {
|
||||||
// there was a delete bug; make the set unmodifiable to catch this earlier
|
// there was a delete bug; make the set unmodifiable to catch this earlier
|
||||||
Set<DomainFile> unmodifiableFiles = Collections.unmodifiableSet(files);
|
Set<DomainFile> unmodifiableFiles = Collections.unmodifiableSet(files);
|
||||||
if (dataTablePanel.isCapacityExceeded()) {
|
if (dataTablePanel.isCapacityExceeded()) {
|
||||||
|
@ -552,7 +552,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
void selectFolder(final DomainFolder folder) {
|
void selectFolder(final DomainFolder folder) {
|
||||||
// Do this later in case any of the given files are newly created, which means that the
|
// Do this later in case any of the given files are newly created, which means that the
|
||||||
// GUIs may have not yet been notified.
|
// GUIs may have not yet been notified.
|
||||||
SwingUtilities.invokeLater(() -> {
|
Swing.runLater(() -> {
|
||||||
projectDataPanel.showTree();
|
projectDataPanel.showTree();
|
||||||
dataTreePanel.selectDomainFolder(folder);
|
dataTreePanel.selectDomainFolder(folder);
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,16 +20,18 @@ import java.util.*;
|
||||||
|
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
|
|
||||||
import docking.widgets.tree.GTreeLazyNode;
|
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
|
import docking.widgets.tree.GTreeSlowLoadingNode;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
import resources.ResourceManager;
|
import resources.ResourceManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to represent a node in the Data tree.
|
* Class to represent a node in the Data tree.
|
||||||
*/
|
*/
|
||||||
public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
|
||||||
|
|
||||||
private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
|
private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
|
||||||
private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
|
private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
|
||||||
|
@ -127,20 +129,24 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<GTreeNode> generateChildren() {
|
public List<GTreeNode> generateChildren(TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
List<GTreeNode> children = new ArrayList<>();
|
List<GTreeNode> children = new ArrayList<>();
|
||||||
if (domainFolder != null && !domainFolder.isEmpty()) {
|
if (domainFolder == null || domainFolder.isEmpty()) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder
|
// NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder
|
||||||
|
|
||||||
DomainFolder[] folders = domainFolder.getFolders();
|
DomainFolder[] folders = domainFolder.getFolders();
|
||||||
for (DomainFolder folder : folders) {
|
for (DomainFolder folder : folders) {
|
||||||
|
monitor.checkCancelled();
|
||||||
children.add(new DomainFolderNode(folder, filter));
|
children.add(new DomainFolderNode(folder, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainFile[] files = domainFolder.getFiles();
|
DomainFile[] files = domainFolder.getFiles();
|
||||||
for (DomainFile domainFile : files) {
|
for (DomainFile domainFile : files) {
|
||||||
|
monitor.checkCancelled();
|
||||||
if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) {
|
if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) {
|
||||||
DomainFolder folder = domainFile.followLink();
|
DomainFolder folder = domainFile.followLink();
|
||||||
if (folder != null) {
|
if (folder != null) {
|
||||||
|
@ -152,7 +158,6 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||||
children.add(new DomainFileNode(domainFile));
|
children.add(new DomainFileNode(domainFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Collections.sort(children);
|
Collections.sort(children);
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,15 @@ import javax.swing.tree.TreeSelectionModel;
|
||||||
|
|
||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
import docking.ComponentProvider;
|
import docking.ComponentProvider;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.*;
|
||||||
import docking.widgets.tree.support.GTreeSelectionListener;
|
import docking.widgets.tree.support.GTreeSelectionListener;
|
||||||
import ghidra.framework.main.FrontEndPlugin;
|
import ghidra.framework.main.FrontEndPlugin;
|
||||||
import ghidra.framework.main.FrontEndTool;
|
import ghidra.framework.main.FrontEndTool;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
import help.Help;
|
import help.Help;
|
||||||
import help.HelpService;
|
import help.HelpService;
|
||||||
|
|
||||||
|
@ -100,6 +102,10 @@ public class ProjectDataTreePanel extends JPanel {
|
||||||
* @param projectData data that has the root folder for the project
|
* @param projectData data that has the root folder for the project
|
||||||
*/
|
*/
|
||||||
public void setProjectData(String projectName, ProjectData projectData) {
|
public void setProjectData(String projectName, ProjectData projectData) {
|
||||||
|
if (this.projectData == projectData) {
|
||||||
|
return; // this can happen during setup if listeners get activated
|
||||||
|
}
|
||||||
|
|
||||||
if (this.projectData != null) {
|
if (this.projectData != null) {
|
||||||
this.projectData.removeDomainFolderChangeListener(changeMgr);
|
this.projectData.removeDomainFolderChangeListener(changeMgr);
|
||||||
}
|
}
|
||||||
|
@ -138,19 +144,6 @@ public class ProjectDataTreePanel extends JPanel {
|
||||||
oldRoot.removeAll();
|
oldRoot.removeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the root data folder (not root node in the tree which
|
|
||||||
* shows the project name).
|
|
||||||
*/
|
|
||||||
public void selectRootDataFolder() {
|
|
||||||
tree.setSelectionPath(root.getTreePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void selectDomainFolder(DomainFolder domainFolder) {
|
|
||||||
TreePath treePath = getTreePath(domainFolder);
|
|
||||||
tree.setSelectionPath(treePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TreePath> getTreePaths(Set<DomainFile> files) {
|
private List<TreePath> getTreePaths(Set<DomainFile> files) {
|
||||||
List<TreePath> results = new ArrayList<>();
|
List<TreePath> results = new ArrayList<>();
|
||||||
for (DomainFile file : files) {
|
for (DomainFile file : files) {
|
||||||
|
@ -177,24 +170,25 @@ public class ProjectDataTreePanel extends JPanel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the root data folder (not root node in the tree which shows the project name).
|
||||||
|
*/
|
||||||
|
public void selectRootDataFolder() {
|
||||||
|
tree.setSelectionPath(root.getTreePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectDomainFolder(DomainFolder domainFolder) {
|
||||||
|
TreePath treePath = getTreePath(domainFolder);
|
||||||
|
tree.expandAndSelectPaths(List.of(treePath));
|
||||||
|
}
|
||||||
|
|
||||||
public void selectDomainFiles(Set<DomainFile> files) {
|
public void selectDomainFiles(Set<DomainFile> files) {
|
||||||
List<TreePath> treePaths = getTreePaths(files);
|
List<TreePath> treePaths = getTreePaths(files);
|
||||||
tree.setSelectionPaths(treePaths);
|
tree.expandAndSelectPaths(treePaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectDomainFile(DomainFile domainFile) {
|
public void selectDomainFile(DomainFile domainFile) {
|
||||||
Iterator<GTreeNode> it = root.iterator(true);
|
selectDomainFiles(Set.of(domainFile));
|
||||||
while (it.hasNext()) {
|
|
||||||
GTreeNode child = it.next();
|
|
||||||
if (child instanceof DomainFileNode) {
|
|
||||||
DomainFile nodeFile = ((DomainFileNode) child).getDomainFile();
|
|
||||||
if (nodeFile.equals(domainFile)) {
|
|
||||||
tree.expandPath(child);
|
|
||||||
tree.setSelectedNode(child);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHelpLocation(HelpLocation helpLocation) {
|
public void setHelpLocation(HelpLocation helpLocation) {
|
||||||
|
@ -487,11 +481,34 @@ public class ProjectDataTreePanel extends JPanel {
|
||||||
* @param s node name
|
* @param s node name
|
||||||
*/
|
*/
|
||||||
public void findAndSelect(String s) {
|
public void findAndSelect(String s) {
|
||||||
if (projectData.getFileCount() < MAX_PROJECT_SIZE_TO_SEARCH) {
|
FindAndSelectTask task = new FindAndSelectTask(tree, s);
|
||||||
tree.expandTree(root);
|
tree.runTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==================================================================================================
|
||||||
|
// Inner Classes
|
||||||
|
//==================================================================================================
|
||||||
|
|
||||||
|
private class FindAndSelectTask extends GTreeTask {
|
||||||
|
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
FindAndSelectTask(GTree gTree, String text) {
|
||||||
|
super(gTree);
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
if (projectData.getFileCount() > MAX_PROJECT_SIZE_TO_SEARCH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (Iterator<GTreeNode> it = root.iterator(true); it.hasNext();) {
|
for (Iterator<GTreeNode> it = root.iterator(true); it.hasNext();) {
|
||||||
|
monitor.checkCancelled();
|
||||||
GTreeNode node = it.next();
|
GTreeNode node = it.next();
|
||||||
if (node.getName().equals(s)) {
|
if (node.getName().equals(text)) {
|
||||||
tree.setSelectedNode(node);
|
tree.setSelectedNode(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -499,10 +516,6 @@ public class ProjectDataTreePanel extends JPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
|
||||||
// Inner Classes
|
|
||||||
//==================================================================================================
|
|
||||||
|
|
||||||
private class MyMouseListener extends MouseAdapter {
|
private class MyMouseListener extends MouseAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void mousePressed(MouseEvent e) {
|
public void mousePressed(MouseEvent e) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue