diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/MergeManagerProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/MergeManagerProvider.java
index c5ab8722f4..d323b5a605 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/MergeManagerProvider.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/MergeManagerProvider.java
@@ -23,6 +23,7 @@ import javax.swing.*;
import docking.ActionContext;
import docking.WindowPosition;
import docking.options.editor.ButtonPanelFactory;
+import docking.util.image.ToolIconURL;
import docking.widgets.OptionDialog;
import docking.widgets.label.*;
import ghidra.app.context.ListingActionContext;
@@ -32,7 +33,6 @@ import ghidra.app.util.HelpTopics;
import ghidra.app.util.viewer.format.FieldHeaderComp;
import ghidra.app.util.viewer.format.FieldHeaderLocation;
import ghidra.framework.plugintool.ComponentProviderAdapter;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.layout.VerticalLayout;
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
index 5b3050488e..a78010c223 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
@@ -22,8 +22,6 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
-import javax.swing.JFrame;
-
import org.jdom.Element;
import docking.ComponentProvider;
@@ -332,6 +330,7 @@ public class TestEnv {
*
This method is considered sub-standard and users should prefer instead
* {@link #launchDefaultTool()} or {@link #launchDefaultTool(Program)}.
*
+ * @param p the program
* @return the newly shown tool
*/
public PluginTool showTool(Program p) {
@@ -357,7 +356,7 @@ public class TestEnv {
/**
* Waits for the first window of the given class. This method is the same as
- * {@link #waitForDialogComponent(Window, Class, int)} with the exception that the parent
+ * {@link #waitForDialogComponent(Class, int)} with the exception that the parent
* window is assumed to be this instance's tool frame.
*
* @param ghidraClass The class of the dialog the user desires
@@ -369,8 +368,7 @@ public class TestEnv {
@Deprecated
public T waitForDialogComponent(Class ghidraClass,
int maxTimeMS) {
- JFrame frame = lazyTool().getToolFrame();
- return AbstractDockingTest.waitForDialogComponent(frame, ghidraClass, maxTimeMS);
+ return AbstractDockingTest.waitForDialogComponent(ghidraClass);
}
private static GhidraProject createGhidraTestProject(String projectName) throws IOException {
@@ -381,7 +379,20 @@ public class TestEnv {
deleteSavedFrontEndTool();
String projectDirectoryName = AbstractGTest.getTestDirectoryPath();
- return GhidraProject.createProject(projectDirectoryName, projectName, true);
+ GhidraProject gp = GhidraProject.createProject(projectDirectoryName, projectName, true);
+
+ //
+ // Unusual Code Alert: The default tool is not always found in the testing environment,
+ // depending upon where the test lives. This code maps the test tool to that tool name
+ // so that tests will have the default tool as needed.
+ //
+ Project project = gp.getProject();
+ ToolChest toolChest = project.getLocalToolChest();
+ ToolTemplate template = getToolTemplate(AbstractGenericTest.DEFAULT_TEST_TOOL_NAME);
+ template.setName(AbstractGenericTest.DEFAULT_TOOL_NAME);
+ AbstractGenericTest.runSwing(() -> toolChest.replaceToolTemplate(template));
+
+ return gp;
}
private static void deleteOldTestTools() {
@@ -466,6 +477,7 @@ public class TestEnv {
/**
* This method differs from {@link #launchDefaultTool()} in that this method does not set the
* tool variable in of this TestEnv instance.
+ * @return the tool
*/
public PluginTool createDefaultTool() {
PluginTool newTool = launchDefaultToolByName(AbstractGenericTest.DEFAULT_TEST_TOOL_NAME);
@@ -497,15 +509,14 @@ public class TestEnv {
return tool;
}
- protected PluginTool launchDefaultToolByName(final String toolName) {
- AtomicReference ref = new AtomicReference<>();
- AbstractGenericTest.runSwing(() -> {
+ protected PluginTool launchDefaultToolByName(String toolName) {
- ToolTemplate toolTemplate =
- ToolUtils.readToolTemplate("defaultTools/" + toolName + ".tool");
+ return AbstractGenericTest.runSwing(() -> {
+
+ ToolTemplate toolTemplate = getToolTemplate(toolName);
if (toolTemplate == null) {
Msg.debug(this, "Unable to find tool: " + toolName);
- return;
+ return null;
}
boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
@@ -515,11 +526,23 @@ public class TestEnv {
Project project = frontEndToolInstance.getProject();
ToolManager toolManager = project.getToolManager();
Workspace workspace = toolManager.getActiveWorkspace();
- ref.set((PluginTool) workspace.runTool(toolTemplate));
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ return (PluginTool) workspace.runTool(toolTemplate);
+ });
+ }
+
+ private static ToolTemplate getToolTemplate(String toolName) {
+
+ return AbstractGenericTest.runSwing(() -> {
+ ToolTemplate toolTemplate =
+ ToolUtils.readToolTemplate("defaultTools/" + toolName + ".tool");
+ if (toolTemplate == null) {
+ Msg.debug(TestEnv.class, "Unable to find tool: " + toolName);
+ return null;
+ }
+ return toolTemplate;
});
- return ref.get();
}
public ScriptTaskListener runScript(File script) throws PluginException {
@@ -550,6 +573,7 @@ public class TestEnv {
/**
* Returns GhidraProject associated with this environment
+ * @return the project
*/
public GhidraProject getGhidraProject() {
return gp;
@@ -559,6 +583,7 @@ public class TestEnv {
* A convenience method to close and then reopen the default project created by this TestEnv
* instance. This will not delete the project between opening and closing and will restore
* the project to its previous state.
+ * @throws IOException if any exception occurs while saving and reopening
*/
public void closeAndReopenProject() throws IOException {
gp.setDeleteOnClose(false);
@@ -579,9 +604,6 @@ public class TestEnv {
return gp.getProjectManager();
}
- /**
- * Returns Project associated with this environment
- */
public Project getProject() {
return gp.getProject();
}
@@ -646,6 +668,8 @@ public class TestEnv {
* the only reason to use this method vice openProgram().
*
* @param programName the name of the program zip file without the ".gzf" extension.
+ * @return the restored domain file
+ * @throws FileNotFoundException if the program file cannot be found
*/
public DomainFile restoreProgram(String programName) throws FileNotFoundException {
DomainFile df = programManager.addProgramToProject(getProject(), programName);
@@ -674,12 +698,12 @@ public class TestEnv {
* @param relativePathName This should be a pathname relative to the "test_resources/testdata"
* director or relative to the "typeinfo" directory. The name should
* include the ".gdt" suffix.
- * @param domainFolder the folder in the test project where the archive should be created.
- * @param monitor monitor for canceling this restore.
+ * @param domainFolder the folder in the test project where the archive should be created
* @return the domain file that was created in the project
+ * @throws Exception if an exception occurs
*/
public DomainFile restoreDataTypeArchive(String relativePathName, DomainFolder domainFolder)
- throws InvalidNameException, IOException, VersionException {
+ throws Exception {
File gdtFile;
try {
@@ -738,10 +762,10 @@ public class TestEnv {
* @param program program object
* @param replace if true any existing cached database with the same name will be replaced
* @param monitor task monitor
- * @throws DuplicateNameException if already cached
+ * @throws Exception if already cached
*/
public void saveToCache(String progName, ProgramDB program, boolean replace,
- TaskMonitor monitor) throws IOException, DuplicateNameException, CancelledException {
+ TaskMonitor monitor) throws Exception {
programManager.saveToCache(progName, program, replace, monitor);
}
@@ -826,7 +850,8 @@ public class TestEnv {
* Launches a tool of the given name using the given domain file.
*
* Note: the tool returned will have auto save disabled by default.
- *
+ *
+ * @param toolName the tool's name
* @return the tool that is launched
*/
public PluginTool launchTool(String toolName) {
diff --git a/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java
new file mode 100644
index 0000000000..e9adecb531
--- /dev/null
+++ b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java
@@ -0,0 +1,604 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package docking.widgets.tree;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.Icon;
+import javax.swing.JComponent;
+
+import org.junit.*;
+
+import docking.*;
+import docking.test.AbstractDockingTest;
+import docking.widgets.filter.*;
+import ghidra.test.DummyTool;
+import ghidra.util.StringUtilities;
+
+public class GTreeFilterTest extends AbstractDockingTest {
+
+ private GTree gTree;
+ private FilterTextField filterField;
+
+ private GTreeRootNode root;
+
+ private DockingWindowManager winMgr;
+
+ @Before
+ public void setUp() throws Exception {
+ root = new TestRootNode();
+ gTree = new GTree(root);
+
+ filterField = (FilterTextField) gTree.getFilterField();
+
+ winMgr = new DockingWindowManager(new DummyTool(), null);
+ winMgr.addComponent(new TestTreeComponentProvider());
+ winMgr.setVisible(true);
+
+ waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ winMgr.dispose();
+ }
+
+ @Test
+ public void testContains() {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ assertEquals("Expected 4 of nodes to be in filtered tree!", 4, root.getChildCount());
+
+ checkContainsNode("ABC");
+ checkContainsNode("XABC");
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("MMM");
+ assertEquals("Expected 4 of nodes to be in filtered tree!", 0, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testMultiWordContains() {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, ' ',
+ MultitermEvaluationMode.AND);
+
+ setFilterText("CX AB");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, ' ', MultitermEvaluationMode.OR);
+
+ setFilterText("CX AB");
+ assertEquals(4, root.getChildCount());
+
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testMultiWordContainsDelimiters() {
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ for (char delim : FilterOptions.VALID_MULTITERM_DELIMITERS.toCharArray()) {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.AND);
+
+ setFilterText("CX" + delim + "AB");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.OR);
+
+ setFilterText("CX" + delim + "AB");
+ assertEquals(4, root.getChildCount());
+
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ }
+
+ @Test
+ public void testMultiWordContainsDelimitersWithLeadingSpaces() {
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ String delimPad = StringUtilities.pad("", ' ', 1);
+
+ for (char delim : FilterOptions.VALID_MULTITERM_DELIMITERS.toCharArray()) {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.AND);
+
+ String delimStr = delimPad + delim;
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.OR);
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(4, root.getChildCount());
+
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+
+ }
+ }
+
+ @Test
+ public void testMultiWordContainsDelimitersWithTrailingSpaces() {
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ String delimPad = StringUtilities.pad("", ' ', 1);
+
+ for (char delim : FilterOptions.VALID_MULTITERM_DELIMITERS.toCharArray()) {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.AND);
+
+ String delimStr = delim + delimPad;
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.OR);
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(4, root.getChildCount());
+
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+
+ }
+ }
+
+ @Test
+ public void testMultiWordContainsDelimitersWithBoundingSpaces() {
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ String delimPad = StringUtilities.pad("", ' ', 1);
+
+ for (char delim : FilterOptions.VALID_MULTITERM_DELIMITERS.toCharArray()) {
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.AND);
+
+ String delimStr = delimPad + delim + delimPad;
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false, true, delim,
+ MultitermEvaluationMode.OR);
+
+ setFilterText("CX" + delimStr + "AB");
+ assertEquals(4, root.getChildCount());
+
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+
+ }
+ }
+
+ @Test
+ public void testInvertedContains() {
+ setFilterOptions(TextFilterStrategy.CONTAINS, true);
+
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ assertEquals(1, root.getChildCount());
+
+ checkDoesNotContainsNode("ABC");
+ checkDoesNotContainsNode("XABC");
+ checkDoesNotContainsNode("ABCX");
+ checkDoesNotContainsNode("XABCX");
+
+ setFilterText("MMM");
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testInvertedMultiWordContains() {
+ setFilterOptions(TextFilterStrategy.CONTAINS, true, true, ' ', MultitermEvaluationMode.AND);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("CX AB");
+
+ checkDoesNotContainsNode("ABCX");
+ checkDoesNotContainsNode("XABCX");
+ assertEquals(3, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, true, true, ' ', MultitermEvaluationMode.OR);
+ setFilterText("");
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("CX AB");
+
+ checkDoesNotContainsNode("ABCX");
+ checkDoesNotContainsNode("XABCX");
+ assertEquals(1, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testStartsWith() {
+ setFilterOptions(TextFilterStrategy.STARTS_WITH, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ checkContainsNode("ABC");
+ checkContainsNode("ABCX");
+ assertEquals(2, root.getChildCount());
+
+ setFilterText("MMM");
+ assertEquals(0, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testInvertedStartsWith() {
+ setFilterOptions(TextFilterStrategy.STARTS_WITH, true);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ checkDoesNotContainsNode("ABC");
+ checkDoesNotContainsNode("ABCX");
+ assertEquals(3, root.getChildCount());
+
+ setFilterText("MMM");
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testExactMatch() {
+ setFilterOptions(TextFilterStrategy.MATCHES_EXACTLY, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ checkContainsNode("ABC");
+ assertEquals(1, root.getChildCount());
+
+ setFilterText("MMM");
+ assertEquals(0, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testInvertedExactMatch() {
+ setFilterOptions(TextFilterStrategy.MATCHES_EXACTLY, true);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("ABC");
+ checkDoesNotContainsNode("ABC");
+ assertEquals(4, root.getChildCount());
+
+ setFilterText("MMM");
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testRegExMatch() {
+ setFilterOptions(TextFilterStrategy.REGULAR_EXPRESSION, false);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("^ABC$");
+ checkContainsNode("ABC");
+ assertEquals("Expected 1 node match exacly match ABC!", 1, root.getChildCount());
+
+ setFilterText("ABC");
+ checkContainsNode("ABC");
+ checkContainsNode("XABC");
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+ assertEquals("Expected 4 of nodes that contain the text ABC!", 4, root.getChildCount());
+
+ setFilterText("XA.{0,2}X");
+ checkContainsNode("XABCX");
+ assertEquals(1, root.getChildCount());
+
+ setFilterText("X{0,1}A.{0,2}X");
+ checkContainsNode("XABCX");
+ checkContainsNode("ABCX");
+ assertEquals(2, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testInvertedRegExMatch() {
+ setFilterOptions(TextFilterStrategy.REGULAR_EXPRESSION, true);
+ // no filter text - make sure all 5 nodes are there
+ assertEquals(5, root.getChildCount());
+
+ setFilterText("^ABC$");
+ checkDoesNotContainsNode("ABC");
+ assertEquals(4, root.getChildCount());
+
+ setFilterText("ABC");
+ checkDoesNotContainsNode("ABC");
+ checkDoesNotContainsNode("XABC");
+ checkDoesNotContainsNode("ABCX");
+ checkDoesNotContainsNode("XABCX");
+ assertEquals(1, root.getChildCount());
+
+ setFilterText("XA.{0,2}X");
+ checkDoesNotContainsNode("XABCX");
+ assertEquals(4, root.getChildCount());
+
+ setFilterText("X{0,1}A.{0,2}X");
+ checkDoesNotContainsNode("XABCX");
+ checkDoesNotContainsNode("ABCX");
+ assertEquals(3, root.getChildCount());
+
+ setFilterText("");
+ assertEquals("Expected all 5 nodes to be back", 5, root.getChildCount());
+ }
+
+ @Test
+ public void testSwitchFilterTypes() {
+ setFilterOptions(TextFilterStrategy.STARTS_WITH, false);
+ setFilterText("ABC");
+ checkContainsNode("ABC");
+ checkContainsNode("ABCX");
+ assertEquals(2, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.MATCHES_EXACTLY, false);
+ checkContainsNode("ABC");
+ assertEquals(1, root.getChildCount());
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ assertEquals("Expected 4 of nodes to be in filtered tree!", 4, root.getChildCount());
+ checkContainsNode("ABC");
+ checkContainsNode("XABC");
+ checkContainsNode("ABCX");
+ checkContainsNode("XABCX");
+
+ }
+
+ @Test
+ public void testSavingSelectedFilterType() {
+ setFilterOptions(TextFilterStrategy.MATCHES_EXACTLY, false);
+ setFilterText("ABC");
+ checkContainsNode("ABC");
+ assertEquals(1, root.getChildCount());
+
+ Object originalValue = getInstanceField("uniquePreferenceKey", gTree);
+ setInstanceField("preferenceKey", gTree.getFilterProvider(), "XYZ");
+ setFilterOptions(TextFilterStrategy.STARTS_WITH, false);
+ checkContainsNode("ABC");
+ checkContainsNode("ABCX");
+ assertEquals(2, root.getChildCount());
+
+ setInstanceField("preferenceKey", gTree.getFilterProvider(), originalValue);
+ setInstanceField("optionsSet", gTree.getFilterProvider(), false);
+ restorePreferences();
+ checkContainsNode("ABC");
+ assertEquals(1, root.getChildCount());
+
+ }
+
+ private void restorePreferences() {
+ runSwing(() -> {
+ GTreeFilterProvider filterProvider = gTree.getFilterProvider();
+ String key = (String) getInstanceField("uniquePreferenceKey", gTree);
+ Class>[] classes = new Class[] { DockingWindowManager.class, String.class };
+ Object[] objs = new Object[] { winMgr, key };
+ invokeInstanceMethod("loadFilterPreference", filterProvider, classes, objs);
+ });
+ waitForTree();
+ }
+
+ private void checkContainsNode(String string) {
+ List children = root.getChildren();
+ for (GTreeNode gTreeNode : children) {
+ if (gTreeNode.getName().equals(string)) {
+ return;
+ }
+ }
+ Assert.fail("Expected node " + string + " to be included in filter, but was not found!");
+ }
+
+ private void checkDoesNotContainsNode(String string) {
+ List children = root.getChildren();
+ for (GTreeNode gTreeNode : children) {
+ if (gTreeNode.getName().equals(string)) {
+ Assert.fail("Expected node " + string +
+ " to be NOT be included in filter, but was not found!");
+ }
+ }
+ }
+
+ private void setFilterText(final String text) {
+ runSwing(() -> {
+ filterField.setText(text);
+ });
+ waitForTree();
+ }
+
+ private void setFilterOptions(final TextFilterStrategy filterStrategy, final boolean inverted) {
+
+ runSwing(() -> {
+ FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted);
+ ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions(
+ filterOptions);
+ });
+ waitForTree();
+
+ }
+
+ private void setFilterOptions(TextFilterStrategy filterStrategy, boolean inverted,
+ boolean multiTerm, char splitCharacter, MultitermEvaluationMode evalMode) {
+ runSwing(() -> {
+ FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted,
+ multiTerm, splitCharacter, evalMode);
+ ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions(
+ filterOptions);
+ });
+ waitForTree();
+ }
+
+ private void waitForTree() {
+ waitForTree(gTree);
+ }
+
+ private class TestRootNode extends AbstractGTreeRootNode {
+
+ TestRootNode() {
+ List children = new ArrayList<>();
+ children.add(new LeafNode("XYZ"));
+ children.add(new LeafNode("ABC"));
+ children.add(new LeafNode("ABCX"));
+ children.add(new LeafNode("XABC"));
+ children.add(new LeafNode("XABCX"));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Root";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A basic leaf node
+ */
+ private class LeafNode extends AbstractGTreeNode {
+
+ private final String name;
+
+ LeafNode(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ class TestTreeComponentProvider extends ComponentProvider {
+
+ public TestTreeComponentProvider() {
+ super(null, "Test", "Test");
+ setDefaultWindowPosition(WindowPosition.STACK);
+ setTabText("Test");
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return gTree;
+ }
+
+ @Override
+ public String getTitle() {
+ return "Test Tree";
+ }
+ }
+
+}
diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java
index 495130750e..55a7615d0b 100644
--- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java
+++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java
@@ -28,6 +28,7 @@ import org.junit.*;
import docking.action.DockingActionIf;
import docking.tool.ToolConstants;
+import docking.util.image.ToolIconURL;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import generic.test.AbstractGTest;
@@ -46,7 +47,6 @@ import ghidra.framework.model.ToolTemplate;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginException;
import ghidra.framework.preferences.Preferences;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.exception.AssertException;
diff --git a/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffSaveSettingsTest.java b/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffSaveSettingsTest.java
index feb26c6366..869f07cf44 100644
--- a/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffSaveSettingsTest.java
+++ b/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffSaveSettingsTest.java
@@ -23,12 +23,12 @@ import org.junit.*;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingAction;
+import docking.util.image.ToolIconURL;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
import ghidra.framework.main.FrontEndPlugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.project.tool.GhidraTool;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.program.database.ProgramDB;
import ghidra.test.ClassicSampleX86ProgramBuilder;
import ghidra.test.TestEnv;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolIconURL.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/ToolIconURL.java
similarity index 99%
rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolIconURL.java
rename to Ghidra/Framework/Docking/src/main/java/docking/util/image/ToolIconURL.java
index 15a06f786c..81cb1739ee 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolIconURL.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/ToolIconURL.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.framework.project.tool;
+package docking.util.image;
import generic.Images;
diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode1Test.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode1Test.java
new file mode 100644
index 0000000000..b2f6d17b45
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode1Test.java
@@ -0,0 +1,309 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package docking.widgets.tree;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.*;
+
+import org.junit.*;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.OptionDialog;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class GTreeSlowLoadingNode1Test extends AbstractDockingTest {
+
+ private static final int MAX_DEPTH = 4;
+ private static final int MIN_CHILD_COUNT = 3;
+ private static final int MAX_CHILD_COUNT = 40;
+
+ private volatile boolean pauseChildLoading = false;
+
+ private JFrame frame;
+ private GTree gTree;
+
+ @Before
+ public void setUp() throws Exception {
+
+ gTree = new GTree(new EmptyRootNode());
+
+ frame = new JFrame("GTree Test");
+ frame.getContentPane().add(gTree);
+ frame.setSize(400, 400);
+ frame.setVisible(true);
+
+ waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ gTree.dispose();
+ frame.dispose();
+ }
+
+ @Test
+ public void testBasicLoading() {
+ gTree.setRootNode(new TestRootNode(100));
+
+ waitForTree();
+
+ // make sure we have some children
+ GTreeRootNode rootNode = gTree.getRootNode();
+ GTreeNode nonLeaf1 = rootNode.getChild(0);
+ assertNotNull(nonLeaf1);
+ GTreeNode leaf1 = rootNode.getChild(1);
+ assertNotNull(leaf1);
+ GTreeNode nonLeaf2 = rootNode.getChild(2);
+ assertNotNull(nonLeaf2);
+
+ int childCount = nonLeaf1.getChildCount();
+ assertTrue("Did not find children for: " + nonLeaf1, childCount > 1);
+ assertEquals("An expected leaf node has some children", 0, leaf1.getChildCount());
+ childCount = nonLeaf2.getChildCount();
+ assertTrue("Did not find children for: " + nonLeaf2, childCount > 1);
+ }
+
+ @Test
+ public void testSlowNodeShowsProgressBar() {
+ gTree.setRootNode(new TestRootNode(5000));
+
+ waitForTree();
+
+ GTreeRootNode rootNode = gTree.getRootNode();
+ GTreeNode nonLeaf1 = rootNode.getChild(0);
+ assertNotNull(nonLeaf1);
+
+ gTree.expandPath(nonLeaf1);
+
+ assertProgressPanel(true);
+
+ assertTrue(nonLeaf1.isInProgress());
+
+ // Press the cancel button on the progress monitor
+ pressProgressPanelCancelButton();
+
+ waitForTree();
+
+ // Verify no progress component
+ assertProgressPanel(false);
+ }
+
+//==================================================================================================
+// Private Methods
+//==================================================================================================
+
+ private void waitForTree() {
+ waitForTree(gTree);
+ }
+
+ private void assertProgressPanel(boolean isShowing) {
+ JComponent panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (!isShowing) {
+ assertNull("Panel is showing when it should not be", panel);
+ return;
+ }
+
+ if (panel == null || !panel.isShowing()) {
+ int maxWaits = 50;// wait a couple seconds, as the progress bar may be delayed
+ int tryCount = 0;
+ while (tryCount < maxWaits) {
+ panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (panel != null && panel.isShowing()) {
+ return;// finally showing!
+ }
+ tryCount++;
+ try {
+ Thread.sleep(50);
+ }
+ catch (Exception e) {
+ // who cares?
+ }
+ }
+ }
+
+ Assert.fail("Progress panel is not showing as expected");
+ }
+
+ private void pressProgressPanelCancelButton() {
+ Object taskMonitorComponent = getInstanceField("monitor", gTree);
+ final JButton cancelButton =
+ (JButton) getInstanceField("cancelButton", taskMonitorComponent);
+ runSwing(() -> cancelButton.doClick(), false);
+
+ OptionDialog confirDialog = waitForDialogComponent(frame, OptionDialog.class, 2000);
+ final JButton confirmCancelButton = findButtonByText(confirDialog, "Yes");
+ runSwing(() -> confirmCancelButton.doClick());
+ }
+
+//==================================================================================================
+// Inner Classes
+//==================================================================================================
+
+ private class EmptyRootNode extends AbstractGTreeRootNode {
+
+ EmptyRootNode() {
+ setChildren(new ArrayList());
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Empty Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ private class TestRootNode extends AbstractGTreeRootNode {
+
+ TestRootNode(int loadDelayMillis) {
+ List children = new ArrayList<>();
+ children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
+ children.add(new TestLeafNode());
+ children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class TestSlowLoadingNode extends GTreeSlowLoadingNode {
+
+ private final long loadDelayMillis;
+ private final int depth;
+
+ TestSlowLoadingNode(long loadDelayMillis, int depth) {
+ this.loadDelayMillis = loadDelayMillis;
+ this.depth = depth;
+ }
+
+ @Override
+ public List generateChildren(TaskMonitor monitor) throws CancelledException {
+
+ if (depth > MAX_DEPTH) {
+ return new ArrayList<>();
+ }
+
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+
+ sleep(loadDelayMillis);
+
+ while (pauseChildLoading) {
+ sleep(100);
+ }
+
+ int childCount = getRandomInt(MIN_CHILD_COUNT, MAX_CHILD_COUNT);
+ List children = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+ int value = getRandomInt(0, 1);
+ if (value == 0) {
+ children.add(new TestSlowLoadingNode(loadDelayMillis, depth + 1));
+ }
+ else {
+ children.add(new TestLeafNode());
+ }
+ }
+ return children;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ }
+
+ private class TestLeafNode extends AbstractGTreeNode {
+
+ private String name = getClass().getSimpleName() + getRandomString();
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode2Test.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode2Test.java
new file mode 100644
index 0000000000..a716d59c07
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNode2Test.java
@@ -0,0 +1,255 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package docking.widgets.tree;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.*;
+
+import org.junit.*;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.filter.FilterTextField;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class GTreeSlowLoadingNode2Test extends AbstractDockingTest {
+
+ private static final int MAX_DEPTH = 4;
+ private static final int MIN_CHILD_COUNT = 3;
+ private static final int MAX_CHILD_COUNT = 3;
+
+ private volatile boolean pauseChildLoading = false;
+
+ private JFrame frame;
+ private GTree gTree;
+ private FilterTextField filterField;
+
+ @Before
+ public void setUp() throws Exception {
+
+ gTree = new GTree(new EmptyRootNode());
+ filterField = (FilterTextField) gTree.getFilterField();
+
+ frame = new JFrame("GTree Test");
+ frame.getContentPane().add(gTree);
+ frame.setSize(400, 400);
+ frame.setVisible(true);
+
+ waitForTree();
+
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ gTree.dispose();
+ frame.dispose();
+ }
+
+ @Test
+ public void testBasicLoading() {
+ gTree.setRootNode(new TestRootNode(0));
+ waitForTree();
+ // make sure we have some children
+ GTreeRootNode rootNode = gTree.getRootNode();
+ List allChildren = rootNode.getAllChildren();
+ typeFilterText("Many B1");
+ clearFilterText();
+ List allChildren2 = rootNode.getAllChildren();
+ assertEquals("Children were reloaded instead of being reused", allChildren, allChildren2);
+ }
+
+//==================================================================================================
+// Private Methods
+//==================================================================================================
+
+ private void typeFilterText(String text) {
+ JTextField textField = (JTextField) getInstanceField("textField", filterField);
+ triggerText(textField, text);
+ waitForTree();
+ }
+
+ private void setFilterText(final String text) {
+ runSwing(() -> filterField.setText(text));
+ waitForTree();
+ }
+
+ private void clearFilterText() {
+ setFilterText("");
+ }
+
+ private void waitForTree() {
+ waitForTree(gTree);
+ }
+
+//==================================================================================================
+// Inner Classes
+//==================================================================================================
+
+ private class EmptyRootNode extends AbstractGTreeRootNode {
+
+ EmptyRootNode() {
+ setChildren(new ArrayList());
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Empty Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ private class TestRootNode extends TestSlowLoadingNode implements GTreeRootNode {
+ private GTree tree;
+
+ TestRootNode(int loadDelayMillis) {
+ super(loadDelayMillis, 3);
+ }
+
+ @Override
+ public void setGTree(GTree tree) {
+ this.tree = tree;
+ }
+
+ @Override
+ public GTree getGTree() {
+ return tree;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class TestSlowLoadingNode extends GTreeSlowLoadingNode {
+
+ private final long loadDelayMillis;
+ private final int depth;
+
+ TestSlowLoadingNode(long loadDelayMillis, int depth) {
+ this.loadDelayMillis = loadDelayMillis;
+ this.depth = depth;
+ }
+
+ @Override
+ public List generateChildren(TaskMonitor monitor) throws CancelledException {
+
+ if (depth > MAX_DEPTH) {
+ return new ArrayList<>();
+ }
+
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+
+ sleep(loadDelayMillis);
+
+ while (pauseChildLoading) {
+ sleep(100);
+ }
+
+ int childCount = getRandomInt(MIN_CHILD_COUNT, MAX_CHILD_COUNT);
+ List children = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+ children.add(new TestSlowLoadingNode(0, depth + 1));
+ }
+ return children;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ }
+
+ private class TestLeafNode extends AbstractGTreeNode {
+
+ private String name = getClass().getSimpleName() + getRandomString();
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNodeTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNodeTest.java
new file mode 100644
index 0000000000..23f1ee6c1c
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeSlowLoadingNodeTest.java
@@ -0,0 +1,309 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package docking.widgets.tree;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.*;
+
+import org.junit.*;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.OptionDialog;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class GTreeSlowLoadingNodeTest extends AbstractDockingTest {
+
+ private static final int MAX_DEPTH = 4;
+ private static final int MIN_CHILD_COUNT = 3;
+ private static final int MAX_CHILD_COUNT = 40;
+
+ private volatile boolean pauseChildLoading = false;
+
+ private JFrame frame;
+ private GTree gTree;
+
+ @Before
+ public void setUp() throws Exception {
+
+ gTree = new GTree(new EmptyRootNode());
+
+ frame = new JFrame("GTree Test");
+ frame.getContentPane().add(gTree);
+ frame.setSize(400, 400);
+ frame.setVisible(true);
+
+ waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ gTree.dispose();
+ frame.dispose();
+ }
+
+ @Test
+ public void testBasicLoading() {
+ gTree.setRootNode(new TestRootNode(100));
+
+ waitForTree();
+
+ // make sure we have some children
+ GTreeRootNode rootNode = gTree.getRootNode();
+ GTreeNode nonLeaf1 = rootNode.getChild(0);
+ assertNotNull(nonLeaf1);
+ GTreeNode leaf1 = rootNode.getChild(1);
+ assertNotNull(leaf1);
+ GTreeNode nonLeaf2 = rootNode.getChild(2);
+ assertNotNull(nonLeaf2);
+
+ int childCount = nonLeaf1.getChildCount();
+ assertTrue("Did not find children for: " + nonLeaf1, childCount > 1);
+ assertEquals("An expected leaf node has some children", 0, leaf1.getChildCount());
+ childCount = nonLeaf2.getChildCount();
+ assertTrue("Did not find children for: " + nonLeaf2, childCount > 1);
+ }
+
+ @Test
+ public void testSlowNodeShowsProgressBar() {
+ gTree.setRootNode(new TestRootNode(5000));
+
+ waitForTree();
+
+ GTreeRootNode rootNode = gTree.getRootNode();
+ GTreeNode nonLeaf1 = rootNode.getChild(0);
+ assertNotNull(nonLeaf1);
+
+ gTree.expandPath(nonLeaf1);
+
+ assertProgressPanel(true);
+
+ assertTrue(nonLeaf1.isInProgress());
+
+ // Press the cancel button on the progress monitor
+ pressProgressPanelCancelButton();
+
+ waitForTree();
+
+ // Verify no progress component
+ assertProgressPanel(false);
+ }
+
+//==================================================================================================
+// Private Methods
+//==================================================================================================
+
+ private void waitForTree() {
+ waitForTree(gTree);
+ }
+
+ private void assertProgressPanel(boolean isShowing) {
+ JComponent panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (!isShowing) {
+ assertNull("Panel is showing when it should not be", panel);
+ return;
+ }
+
+ if (panel == null || !panel.isShowing()) {
+ int maxWaits = 50;// wait a couple seconds, as the progress bar may be delayed
+ int tryCount = 0;
+ while (tryCount < maxWaits) {
+ panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (panel != null && panel.isShowing()) {
+ return;// finally showing!
+ }
+ tryCount++;
+ try {
+ Thread.sleep(50);
+ }
+ catch (Exception e) {
+ // who cares?
+ }
+ }
+ }
+
+ Assert.fail("Progress panel is not showing as expected");
+ }
+
+ private void pressProgressPanelCancelButton() {
+ Object taskMonitorComponent = getInstanceField("monitor", gTree);
+ final JButton cancelButton =
+ (JButton) getInstanceField("cancelButton", taskMonitorComponent);
+ runSwing(() -> cancelButton.doClick(), false);
+
+ OptionDialog confirDialog = waitForDialogComponent(OptionDialog.class);
+ final JButton confirmCancelButton = findButtonByText(confirDialog, "Yes");
+ runSwing(() -> confirmCancelButton.doClick());
+ }
+
+//==================================================================================================
+// Inner Classes
+//==================================================================================================
+
+ private class EmptyRootNode extends AbstractGTreeRootNode {
+
+ EmptyRootNode() {
+ setChildren(new ArrayList());
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Empty Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ private class TestRootNode extends AbstractGTreeRootNode {
+
+ TestRootNode(int loadDelayMillis) {
+ List children = new ArrayList<>();
+ children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
+ children.add(new TestLeafNode());
+ children.add(new TestSlowLoadingNode(loadDelayMillis, 1));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class TestSlowLoadingNode extends GTreeSlowLoadingNode {
+
+ private final long loadDelayMillis;
+ private final int depth;
+
+ TestSlowLoadingNode(long loadDelayMillis, int depth) {
+ this.loadDelayMillis = loadDelayMillis;
+ this.depth = depth;
+ }
+
+ @Override
+ public List generateChildren(TaskMonitor monitor) throws CancelledException {
+
+ if (depth > MAX_DEPTH) {
+ return new ArrayList<>();
+ }
+
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+
+ sleep(loadDelayMillis);
+
+ while (pauseChildLoading) {
+ sleep(100);
+ }
+
+ int childCount = getRandomInt(MIN_CHILD_COUNT, MAX_CHILD_COUNT);
+ List children = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ if (monitor.isCancelled()) {
+ return new ArrayList<>();
+ }
+ int value = getRandomInt(0, 1);
+ if (value == 0) {
+ children.add(new TestSlowLoadingNode(loadDelayMillis, depth + 1));
+ }
+ else {
+ children.add(new TestLeafNode());
+ }
+ }
+ return children;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ }
+
+ private class TestLeafNode extends AbstractGTreeNode {
+
+ private String name = getClass().getSimpleName() + getRandomString();
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java
new file mode 100644
index 0000000000..895effedce
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java
@@ -0,0 +1,1273 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package docking.widgets.tree;
+
+import static org.junit.Assert.*;
+
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.tree.TreePath;
+
+import org.junit.*;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.OptionDialog;
+import docking.widgets.filter.FilterOptions;
+import docking.widgets.filter.TextFilterStrategy;
+import docking.widgets.tree.support.GTreeFilter;
+
+public class GTreeTest extends AbstractDockingTest {
+
+ private JFrame frame;
+ private GTree gTree;
+ private int nodeIdCounter = 1;
+
+ /**
+ * Test variable to easily control the filtering of test filters.
+ */
+ private volatile boolean filterEnabled = true;
+
+ @Before
+ public void setUp() throws Exception {
+ gTree = new GTree(new TestRootNode());
+
+ frame = new JFrame("GTree Test");
+ frame.getContentPane().add(gTree);
+ frame.setSize(400, 400);
+ frame.setVisible(true);
+
+ waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ gTree.dispose();
+ frame.dispose();
+ }
+
+ @Test
+ public void testFilterFromKeyStroke() {
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ GTreeNode node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ typeFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+
+ clearFilterText();
+
+ findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ typeFilterText("Zoolander");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNull("Found a node in the tree that should have been filtered out", node);
+ }
+
+ @Test
+ public void testFilterFromSetFilterText() {
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ GTreeNode node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ setFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+
+ clearFilterText();
+
+ findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ setFilterText("Zoolander");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNull("Found a node in the tree that should have been filtered out", node);
+ }
+
+ @Test
+ public void testRefilter() {
+ final GTreeFilterProvider provider = new DefaultGTreeFilterProvider(gTree) {
+ @Override
+ public GTreeFilter getFilter() {
+ if (filterEnabled) {
+ return super.getFilter();
+ }
+ return new DisabledGTreeFilter();
+ }
+ };
+ // Set the filter provider
+ runSwing(() -> gTree.setFilterProvider(provider));
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+ //
+ // Some basic filter tests
+ //
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ GTreeNode node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ setFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+
+ clearFilterText();
+
+ findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ setFilterText("Zoolander");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNull("Found a node in the tree that should have been filtered out", node);
+
+ //
+ // Now try filtering and re-filtering
+ //
+ setFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+
+ gTree.refilter();
+ waitForTree();
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+
+ filterEnabled = false;
+ gTree.refilter();
+ waitForTree();
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNull("Found node when filter should not have matched after a refilter", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+ }
+
+ @Test
+ public void testChangeFilterSettingsWithFilterTextInPlace() {
+
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ GTreeNode node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node2);
+
+ typeFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ setFilterOptions(TextFilterStrategy.CONTAINS, false);
+
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ node2 = findNodeInTree("Leaf Child - Single B1");
+ assertNull("Found a node in the tree that should have been filtered out", node2);
+ }
+
+ @Test
+ public void testSelectionReturnsAfterFiltering() {
+ //
+ // Test that a selection made before a filter is still there after a filter
+ //
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+
+ TreePath selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ setFilterText("No match text");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNull("Found node that should have not passed filter", node);
+
+ selectionPath = gTree.getSelectionPath();
+ assertNull("Selection remained in tree after filter", selectionPath);
+
+ clearFilterText();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertEquals(node, selectionPath.getLastPathComponent());
+ }
+
+ @Test
+ public void testSelectionDuringFilterReturnsAfterFilter() {
+ //
+ // Test that a selection made during a filter returns after the filter is removed.
+ //
+ setFilterText("Many B1");
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+ TreePath selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ clearFilterText();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ //
+ // Same test, but with a selection that existed before the filter's selection change
+ //
+ gTree.clearSelectionPaths();
+ waitForTree();
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ setFilterText("ChildrenNodeA");// filter to show the node below
+
+ GTreeNode anotherNode =
+ findNodeInTree(NonLeafWithOneLevelOfChildrenNodeA.class.getSimpleName());
+ assertNotNull("Could not find expected node", anotherNode);
+
+ gTree.setSelectedNode(anotherNode);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(anotherNode, selectionPath.getLastPathComponent());
+
+ clearFilterText();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(anotherNode, selectionPath.getLastPathComponent());
+ }
+
+ @Test
+ public void testProgrammaticSelectAndFilter() {
+ //
+ // Open a node and select it. Make sure it is again selected if it passes the filter.
+ //
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+
+ TreePath selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ setFilterText("Many B1");
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in filtered tree", node);
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Node matching filter should still be selected after filter", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ clearFilterText();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ node = findNodeInTree("Leaf Child - Many B1");
+ assertEquals(node, selectionPath.getLastPathComponent());
+ }
+
+ @Test
+ public void testSetSelectedPaths() {
+ TreePath selectionPath = gTree.getSelectionPath();
+ assertNull("Unexpectedly have a selected path by default", selectionPath);
+
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ gTree.clearSelectionPaths();
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNull("Selection paths not cleared", selectionPath);
+
+ //
+ // now try calling select node while another node is already selected
+ //
+ GTreeNode anotherNode =
+ findNodeInTree(NonLeafWithOneLevelOfChildrenNodeA.class.getSimpleName());
+ assertNotNull("Could not find expected node", anotherNode);
+
+ gTree.setSelectedNode(node);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(node, selectionPath.getLastPathComponent());
+
+ gTree.setSelectedNode(anotherNode);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(anotherNode, selectionPath.getLastPathComponent());
+
+ //
+ // try multiple paths
+ //
+ List pathsToSelect = new ArrayList<>();
+ pathsToSelect.add(node.getTreePath());
+ pathsToSelect.add(anotherNode.getTreePath());
+
+ gTree.setSelectionPaths(pathsToSelect);
+ waitForTree();
+
+ TreePath[] paths = gTree.getSelectionPaths();
+ assertEquals("Found non-matching selected path count afterr calling setSelectionPaths",
+ pathsToSelect.size(), paths.length);
+ for (int i = 0; i < paths.length; i++) {
+ assertEquals("Unexpected path selection", pathsToSelect.get(i), paths[i]);
+ }
+ }
+
+ @Test
+ public void testExpandPath() {
+ List expandedPaths = gTree.getExpandedPaths(gTree.getRootNode());
+ assertEquals("Unexpectedly have an expanded tree by default", 1, expandedPaths.size());
+ assertEquals(gTree.getRootNode(), expandedPaths.get(0).getLastPathComponent());
+
+ GTreeNode node = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", node);
+
+ gTree.expandPath(node);
+ waitForTree();
+
+ expandedPaths = gTree.getExpandedPaths(gTree.getRootNode());
+ assertTrue("Did not get expanded paths after calling expand paths",
+ expandedPaths.size() > 0);
+ assertExpaned(expandedPaths, node.getParent());
+ }
+
+ @Test
+ public void testPreventRootCollapseExpandsRoot() {
+ gTree.collapseAll(gTree.getRootNode());
+ waitForTree();
+
+ gTree.setRootNodeAllowedToCollapse(false);
+ waitForTree();
+
+ List expanded = gTree.getExpandedPaths(gTree.getRootNode());
+
+ assertEquals("Should have only one expanded path", 1, expanded.size());
+ TreePath path0 = expanded.get(0);
+
+ assertEquals(path0.getLastPathComponent(), gTree.getRootNode());
+ assertEquals("Expanded path does not end at tree root", path0.getLastPathComponent(),
+ gTree.getRootNode());
+ }
+
+ @Test
+ public void testPreventRootCollapseAttemptsRootCollapse() {
+ gTree.setRootNodeAllowedToCollapse(false);
+
+ gTree.expandAll();
+ waitForTree();
+
+ gTree.collapseAll(gTree.getRootNode());
+ waitForTree();
+
+ List expanded = gTree.getExpandedPaths(gTree.getRootNode());
+
+ assertEquals("Should have only one expanded path", 1, expanded.size());
+ TreePath path0 = expanded.get(0);
+
+ assertEquals("Expanded path does not end at tree root", path0.getLastPathComponent(),
+ gTree.getRootNode());
+ }
+
+ @Test
+ public void testCancelFilter() {
+ //
+ // Setup a long running filter and make sure that the progress bar is show. Then, cancel
+ // that filter and make sure that the filter is cleared and the progress bar is hidden.
+ //
+
+ final ReallySlowGTreeFilter filter = new ReallySlowGTreeFilter();
+ final GTreeFilterProvider provider = new DefaultGTreeFilterProvider(gTree) {
+ @Override
+ public GTreeFilter getFilter() {
+ GTreeFilter superFilter = super.getFilter();
+ if (superFilter == null) {
+ return null;
+ }
+ return filter;
+ }
+ };
+ provider.setFilterText("Hey");
+ // Set the filter (Note: don't use setTextFilterFactory(), as it waits for the tree,
+ // which is not what this test wants to do)
+ runSwing(() -> gTree.setFilterProvider(provider));
+
+ // Wait for the tree to be 'busy'
+ waitForTreeToStartWork();
+
+ // Verify the progress monitor is showing
+ assertProgressPanel(true);
+
+ // Press the cancel button on the progress monitor
+ pressProgressPanelCancelButton();
+
+ waitForTree();
+
+ // Verify no filter
+ String filterText = gTree.getFilterText();
+ assertEquals("", filterText);
+
+ // Verify no progress component
+ assertProgressPanel(false);
+ }
+
+ @Test
+ public void testGetAndRestoreTreeState() {
+ //
+ // Test that we can setup the tree, record its state, change the tree and then restore
+ // the saved state
+ //
+ TreePath selectionPath = gTree.getSelectionPath();
+ assertNull("Unexpectedly have a selected path by default", selectionPath);
+
+ GTreeNode originalNode = findNodeInTree("Leaf Child - Many B1");
+ assertNotNull("Did not find existing child node in non filtered tree", originalNode);
+
+ gTree.setSelectedNode(originalNode);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(originalNode, selectionPath.getLastPathComponent());
+
+ GTreeState treeState = gTree.getTreeState();
+
+ gTree.clearSelectionPaths();
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNull("Selection paths not cleared", selectionPath);
+
+ GTreeNode anotherNode =
+ findNodeInTree(NonLeafWithOneLevelOfChildrenNodeA.class.getSimpleName());
+ assertNotNull("Could not find expected node", anotherNode);
+
+ gTree.setSelectedNode(anotherNode);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(anotherNode, selectionPath.getLastPathComponent());
+
+ gTree.restoreTreeState(treeState);
+ waitForTree();
+
+ selectionPath = gTree.getSelectionPath();
+ assertNotNull("Tree did not select node", selectionPath);
+ assertEquals(originalNode, selectionPath.getLastPathComponent());
+ }
+
+ @Test
+ public void testFilterPathsRestoredWithFurtherFiltering_NoSelection() throws Exception {
+
+ installLargeTreeModel();
+
+ // this filter allows all children to stay in the view
+ setFilterText("Leaf");
+
+ TreePath firstVisiblePath = scrollTo(50);
+
+ setFilterText("Leaf Child");
+
+ TreePath newFirstVisiblePath = getLastVisiblePath();
+ assertCloseEnough(firstVisiblePath, newFirstVisiblePath);
+ }
+
+ @Test
+ public void testFilterPathsRestoredWithFurtherFiltering_HaveSelection() throws Exception {
+
+ //
+ // Test to verify that the selection restoration takes precedence over the 'visible
+ // view' restoration.
+ //
+
+ installLargeTreeModel();
+
+ // this filter allows all children to stay in the view
+ setFilterText("Leaf");
+
+ selectRow(5);
+
+ TreePath expected = getLastVisiblePath();
+
+ scrollTo(50);
+
+ setFilterText("Leaf Child");
+
+ TreePath newFirstVisiblePath = getLastVisiblePath();
+ assertCloseEnough(expected, newFirstVisiblePath);
+ }
+
+ @Test
+ public void testFilterPathsRestored_WhenTooManyItemsExpanded_NoSelection() {
+
+ //
+ // Tests the algorithm for throwing away expanded paths when there are too many to
+ // restore.
+ //
+
+ installLargeTreeModel_WithManyExpandablePaths();
+
+ // generate more than 'max' expanded states to force the algorithm to toss some states
+ expandAll();
+
+ scrollTo(50);
+
+ TreePath expected = getLastVisiblePath();
+
+ // keep all items in filter; move the view to the top
+ setFilterText("a");
+
+ // verify that the view contains the expanded path
+ TreePath newFirstVisiblePath = getLastVisiblePath();
+ assertCloseEnough(expected, newFirstVisiblePath);
+ }
+
+ @Test
+ public void testFilterPathsRestored_WhenTooManyItemsExpanded_HaveSelection() {
+
+ //
+ // Tests the algorithm for throwing away expanded paths when there are too many to
+ // restore.
+ //
+
+ installLargeTreeModel_WithManyExpandablePaths();
+
+ // generate more than 'max' expanded and selected states to force the algorithm
+ // to toss some states
+ expandAll();
+ int topRow = 50;
+
+ scrollTo(topRow);
+ selectRow(topRow);
+
+ // keep all items in filter; move the view to the top
+ setFilterText("a");
+
+ // verify that the view contains the selected path
+ TreePath selectedPath = getSelectedPath();
+ assertCloseEnough(rowToPath(topRow), selectedPath);
+ }
+
+ @Test
+ public void testFilterPathsRestored_WhenTooManyItemsExpanded_HaveTooManyItemsSelected() {
+
+ //
+ // Tests the algorithm for throwing away expanded paths when there are too many to
+ // restore.
+ //
+
+ installLargeTreeModel_WithManyExpandablePaths();
+
+ // generate more than 'max' expanded and selected states to force the algorithm
+ // to toss some states
+ expandAll();
+ int topRow = 50;
+
+ scrollTo(topRow);
+ selectContiguousRows(0, topRow + 5);
+
+ // keep all items in filter; move the view to the top
+ setFilterText("a");
+
+ // verify that the view contains the selected path
+ TreePath selectedPath = getSelectedPath();
+ assertCloseEnough(rowToPath(topRow), selectedPath);
+ }
+
+//==================================================================================================
+// Private methods
+//==================================================================================================
+
+ private void installLargeTreeModel() {
+ runSwing(() -> frame.getContentPane().remove(gTree));
+ gTree = new TestGTree(new ManyLeafChildrenRootNode());
+ runSwing(() -> frame.getContentPane().add(gTree));
+
+ frame.validate();
+ }
+
+ private void installLargeTreeModel_WithManyExpandablePaths() {
+ runSwing(() -> frame.getContentPane().remove(gTree));
+ gTree = new TestGTree(new ManyNonLeafChildrenRootNode());
+ runSwing(() -> frame.getContentPane().add(gTree));
+
+ frame.validate();
+ }
+
+ private void expandAll() {
+ gTree.expandAll();
+ waitForTree();
+ }
+
+ private void selectRow(int row) {
+ TreePath path = rowToPath(row);
+ gTree.setSelectedNodeByPathName(path);
+ }
+
+ private void selectContiguousRows(int from, int to) {
+
+ // hack: process the list backwards so that the 'to' row is what ends up in the view--the
+ // tree will scroll to the first selected element
+ List paths = new ArrayList<>();
+ for (int row = to; row >= from; row--) {
+ paths.add(rowToPath(row));
+ }
+
+ gTree.setSelectionPaths(paths);
+ waitForTree();
+ }
+
+ private TreePath rowToPath(int row) {
+ TreePath path = gTree.getPathForRow(row);
+ return path;
+ }
+
+ private void assertExpaned(List expandedPaths, GTreeNode expected) {
+
+ for (TreePath path : expandedPaths) {
+ GTreeNode node = (GTreeNode) path.getLastPathComponent();
+ if (expected.equals(node)) {
+ return;
+ }
+ }
+
+ fail("Node not expaded: " + expected + "; expanded paths: " + expandedPaths);
+ }
+
+ /** Verifies the actual is within one of the expected */
+ private void assertCloseEnough(TreePath expected, TreePath actual) {
+
+ if (actual == null) {
+ fail("No actual TreePath found");
+ }
+
+ if (expected.equals(actual)) {
+ return; // good
+ }
+
+ // Unusual Code: we add some 'fudge' to the viewable tree rows (1 in the top and
+ // bottom direction). So, the actual path visible may be off by that
+ // much.
+ int fudge = 2; // 1 in both directions
+ int expectedRow = gTree.getRowForPath(expected);
+ if (rowNumberIsWithin(expected, actual, fudge)) {
+ return; // good enough
+ }
+
+ int actualRow = gTree.getRowForPath(actual);
+ fail("Tree paths do not match - expected row " + expectedRow + "; actual row " + actualRow);
+ }
+
+ private boolean rowNumberIsWithin(TreePath expected, TreePath actual, int fudge) {
+
+ //int expectedRow = gTree.getRowForPath(expected);
+ int actualRow = gTree.getRowForPath(actual);
+
+ // first, go up
+ for (int offset = 0; offset > fudge; offset--) {
+ int previousRow = actualRow - offset;
+ TreePath previousPath = gTree.getPathForRow(previousRow);
+ if (actual.equals(previousPath)) {
+ return true; // good enough
+ }
+ }
+
+ // next, go down
+ for (int offset = 0; offset < fudge; offset++) {
+ int previousRow = actualRow + offset;
+ TreePath nextPath = gTree.getPathForRow(previousRow);
+ if (actual.equals(nextPath)) {
+ return true; // good enough
+ }
+ }
+
+ return false;
+ }
+
+ private TreePath scrollTo(int row) {
+ runSwing(() -> {
+ TreePath path = gTree.getPathForRow(row);
+ gTree.scrollPathToVisible(path);
+ });
+
+ TreePath path = getLastVisiblePath();
+ return path;
+ }
+
+ private TreePath getLastVisiblePath() {
+ Rectangle r = gTree.getViewRect();
+
+ JTree jTree = gTree.getJTree();
+ int start = jTree.getClosestRowForLocation(r.x, r.y + r.height);
+ TreePath path = gTree.getPathForRow(start);
+ return path;
+ }
+
+ @Override
+ protected void testFailed(Throwable e) {
+ StringBuilder buffy = new StringBuilder();
+ printTree(gTree.getRootNode(), 0, buffy);
+ System.err.println("GTree state: ");
+ System.err.println(buffy.toString());
+ }
+
+ private void printTree(GTreeNode node, int indentLevel, StringBuilder buffy) {
+ buffy.append('\n');
+ for (int i = 0; i < indentLevel; i++) {
+ buffy.append('\t');
+ }
+ buffy.append("node: " + node.getName());
+ int nextIndentLevel = indentLevel + 1;
+ List children = node.getChildren();
+ for (GTreeNode child : children) {
+ printTree(child, nextIndentLevel, buffy);
+ }
+ }
+
+ private void assertProgressPanel(boolean isShowing) {
+ JComponent panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (!isShowing) {
+ assertNull("Panel is showing when it should not be", panel);
+ return;
+ }
+
+ if (panel == null || !panel.isShowing()) {
+ int maxWaits = 50;// wait a couple seconds, as the progress bar may be delayed
+ int tryCount = 0;
+ while (tryCount < maxWaits) {
+ panel = (JComponent) getInstanceField("progressPanel", gTree);
+ if (panel != null && panel.isShowing()) {
+ return;// finally showing!
+ }
+ tryCount++;
+ try {
+ Thread.sleep(50);
+ }
+ catch (Exception e) {
+ // who cares?
+ }
+ }
+ }
+
+ Assert.fail("Progress panel is not showing as expected");
+ }
+
+ private void pressProgressPanelCancelButton() {
+ Object taskMonitorComponent = getInstanceField("monitor", gTree);
+ final JButton cancelButton =
+ (JButton) getInstanceField("cancelButton", taskMonitorComponent);
+ runSwing(() -> cancelButton.doClick(), false);
+
+ OptionDialog confirDialog = waitForDialogComponent(OptionDialog.class);
+ JButton confirmCancelButton = findButtonByText(confirDialog, "Yes");
+ runSwing(() -> confirmCancelButton.doClick());
+ }
+
+ private void waitForTree() {
+ waitForTree(gTree);
+ }
+
+ private void waitForTreeToStartWork() {
+
+ waitForCondition(() -> gTree.isBusy(),
+ "Tree did not start filtering task or finished too soon");
+ }
+
+ private void typeFilterText(String text) {
+ Component filterField = gTree.getFilterField();
+ JTextField textField = (JTextField) getInstanceField("textField", filterField);
+ triggerText(textField, text);
+ waitForTree();
+ }
+
+ private void setFilterText(final String text) {
+ runSwing(() -> gTree.setFilterText(text));
+ waitForTree();
+ }
+
+ private void clearFilterText() {
+ setFilterText("");
+ }
+
+ private TreePath getSelectedPath() {
+ waitForTree();
+ return gTree.getSelectionPath();
+ }
+
+ private GTreeNode findNodeInTree(String name) {
+ GTreeRootNode rootNode = gTree.getRootNode();
+ return findNodeInTree(rootNode, name);
+ }
+
+ private GTreeNode findNodeInTree(GTreeNode node, String name) {
+ if (node.getName().equals(name)) {
+ return node;
+ }
+
+ List children = node.getChildren();
+ for (GTreeNode child : children) {
+ if (child.getName().startsWith(name)) {
+ return child;
+ }
+
+ GTreeNode grandChild = findNodeInTree(child, name);
+ if (grandChild != null) {
+ return grandChild;
+ }
+ }
+
+ return null;
+ }
+
+ private void setFilterOptions(final TextFilterStrategy filterStrategy, final boolean inverted) {
+ runSwing(() -> {
+ FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted);
+ ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions(
+ filterOptions);
+ });
+ waitForTree();
+
+ }
+
+//==================================================================================================
+// Inner Classes
+//==================================================================================================
+
+ private class TestGTree extends GTree {
+
+ public TestGTree(GTreeRootNode root) {
+ super(root);
+ }
+
+ @Override
+ public GTreeState getTreeState() {
+ return new GTreeState(this) {
+ @Override
+ int getMaxItemCount() {
+ return 5; // smaller number for testing
+ }
+ };
+ }
+
+ @Override
+ public GTreeState getTreeState(GTreeNode node) {
+ return new GTreeState(this, node) {
+ @Override
+ int getMaxItemCount() {
+ return 5; // smaller number for testing
+ }
+ };
+ }
+ }
+
+ private class TestRootNode extends AbstractGTreeRootNode {
+
+ TestRootNode() {
+ List children = new ArrayList<>();
+ children.add(new NonLeafWithOneLevelOfChildrenNodeA());
+ children.add(new LeafNode("Leaf Child - Root1"));
+ children.add(new NonLeafWithManyLevelOfChildrenNodeA());
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class ManyLeafChildrenRootNode extends AbstractGTreeRootNode {
+
+ ManyLeafChildrenRootNode() {
+ List children = new ArrayList<>();
+ children.add(new NonLeafWithMoreChildrenThanFitInTheView(true));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class ManyNonLeafChildrenRootNode extends AbstractGTreeRootNode {
+
+ ManyNonLeafChildrenRootNode() {
+ List children = new ArrayList<>();
+ children.add(new NonLeafWithMoreChildrenThanFitInTheView(false));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "Test GTree Root Node";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A basic node with some children.
+ */
+ private class NonLeafWithOneLevelOfChildrenNodeA extends AbstractGTreeNode {
+
+ private String name = Integer.toString(++nodeIdCounter);
+
+ NonLeafWithOneLevelOfChildrenNodeA() {
+ List children = new ArrayList<>();
+ setChildren(children);
+
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName() + " (" + name + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A basic node with some children.
+ */
+ private class NonLeafWithOneLevelOfChildrenNodeB extends AbstractGTreeNode {
+ private String name = Integer.toString(++nodeIdCounter);
+
+ NonLeafWithOneLevelOfChildrenNodeB() {
+ this(1);
+ }
+
+ NonLeafWithOneLevelOfChildrenNodeB(int n) {
+ List children = new ArrayList<>();
+ name = Integer.toString(n);
+ children.add(new LeafNode("Leaf Child - Single B" + n));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName() + " (" + name + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A basic leaf node
+ */
+ private class LeafNode extends AbstractGTreeNode {
+
+ private final String name;
+
+ LeafNode(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ /**
+ * A node with children that may have children
+ */
+ private class NonLeafWithManyLevelOfChildrenNodeA extends AbstractGTreeNode {
+
+ private String name = Integer.toString(++nodeIdCounter);
+
+ NonLeafWithManyLevelOfChildrenNodeA() {
+ List children = new ArrayList<>();
+ children.add(new NonLeafWithManyLevelOfChildrenNodeB());
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName() + " (" + name + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A node with children that may have children
+ */
+ private class NonLeafWithManyLevelOfChildrenNodeB extends AbstractGTreeNode {
+
+ private String name = Integer.toString(++nodeIdCounter);
+
+ NonLeafWithManyLevelOfChildrenNodeB() {
+ List children = new ArrayList<>();
+ children.add(new NonLeafWithOneLevelOfChildrenNodeB());
+ children.add(new LeafNode("Leaf Child - Many B1"));
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName() + " (" + name + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ /**
+ * A basic node with some children.
+ */
+ private class NonLeafWithMoreChildrenThanFitInTheView extends AbstractGTreeNode {
+
+ private String name = Integer.toString(++nodeIdCounter);
+
+ NonLeafWithMoreChildrenThanFitInTheView(boolean childrenAreLeaves) {
+ List children = new ArrayList<>();
+
+ for (int i = 0; i < 100; i++) {
+
+ if (childrenAreLeaves) {
+ children.add(new LeafNode("Leaf Child - " + i));
+ }
+ else {
+ children.add(new NonLeafWithOneLevelOfChildrenNodeB(i));
+ }
+
+ }
+
+ setChildren(children);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName() + "(" + name + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ private class DisabledGTreeFilter implements GTreeFilter {
+
+ @Override
+ public boolean acceptsNode(GTreeNode node) {
+ return false;
+ }
+
+ @Override
+ public boolean showFilterMatches() {
+ return false;
+ }
+
+ }
+
+ private class ReallySlowGTreeFilter implements GTreeFilter {
+
+ @Override
+ public boolean acceptsNode(GTreeNode node) {
+ if (!SwingUtilities.isEventDispatchThread()) {
+ // this filter is called by the worker thread AND the swing thread for rendering,
+ // make sure not to wait in the swing thread
+ sleep(2000);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean showFilterMatches() {
+ return false;
+ }
+
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
index b209b92556..e525a19686 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
@@ -62,6 +62,7 @@ public abstract class AbstractGenericTest extends AbstractGTest {
private static File debugDirectory;
public static final String TESTDATA_DIRECTORY_NAME = "testdata";
+ public static final String DEFAULT_TOOL_NAME = "CodeBrowser";
public static final String DEFAULT_TEST_TOOL_NAME = "TestCodeBrowser";
private static boolean initialized = false;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
index 7c67117b8b..f507be5de8 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
@@ -40,6 +40,7 @@ import docking.help.Help;
import docking.help.HelpService;
import docking.tool.ToolConstants;
import docking.util.AnimationUtils;
+import docking.util.image.ToolIconURL;
import docking.widgets.OptionDialog;
import generic.jar.ResourceFile;
import generic.util.WindowUtilities;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
index 7c561932aa..5fbae30216 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
@@ -32,11 +32,11 @@ import docking.dnd.*;
import docking.help.Help;
import docking.help.HelpService;
import docking.tool.ToolConstants;
+import docking.util.image.ToolIconURL;
import docking.widgets.EmptyBorderButton;
import ghidra.framework.main.datatree.*;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.util.*;
import ghidra.util.bean.GGlassPane;
import ghidra.util.exception.AssertException;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Tool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Tool.java
index 823ab8141d..864e2ebd6d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Tool.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Tool.java
@@ -21,8 +21,8 @@ import java.beans.PropertyVetoException;
import org.jdom.Element;
import docking.DockingTool;
+import docking.util.image.ToolIconURL;
import ghidra.framework.plugintool.PluginEvent;
-import ghidra.framework.project.tool.ToolIconURL;
/**
*
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolTemplate.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolTemplate.java
index 815151c575..b211aec18c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolTemplate.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolTemplate.java
@@ -19,7 +19,7 @@ import javax.swing.ImageIcon;
import org.jdom.Element;
-import ghidra.framework.project.tool.ToolIconURL;
+import docking.util.image.ToolIconURL;
/**
* Configuration of a tool that knows how to create tools.
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
index 0abe43fc2f..91d89fdaa9 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
@@ -42,6 +42,7 @@ import docking.help.Help;
import docking.help.HelpService;
import docking.tool.ToolConstants;
import docking.tool.util.DockingToolConstants;
+import docking.util.image.ToolIconURL;
import docking.widgets.OptionDialog;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
@@ -56,7 +57,6 @@ import ghidra.framework.plugintool.dialog.ManagePluginsDialog;
import ghidra.framework.plugintool.mgr.*;
import ghidra.framework.plugintool.util.*;
import ghidra.framework.project.ProjectDataService;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.util.*;
import ghidra.util.task.Task;
import ghidra.util.task.TaskLauncher;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/IconMap.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/IconMap.java
index cc7c8bd5ea..20497c60b6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/IconMap.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/IconMap.java
@@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +15,9 @@
*/
package ghidra.framework.plugintool.dialog;
-import ghidra.framework.project.tool.ToolIconURL;
-
import java.util.*;
+import docking.util.image.ToolIconURL;
import resources.ResourceManager;
/**
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java
index 0320960714..d7d0a626ec 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java
@@ -27,6 +27,7 @@ import javax.swing.event.*;
import docking.DialogComponentProvider;
import docking.options.editor.ButtonPanelFactory;
+import docking.util.image.ToolIconURL;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.label.GLabel;
@@ -34,7 +35,6 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.project.tool.GhidraToolTemplate;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.util.HelpLocation;
import ghidra.util.NamingUtilities;
import ghidra.util.filechooser.ExtensionFileFilter;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ToolIconUrlRenderer.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ToolIconUrlRenderer.java
index 036a349485..b18ccdfd25 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ToolIconUrlRenderer.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ToolIconUrlRenderer.java
@@ -22,8 +22,8 @@ import javax.swing.BorderFactory;
import javax.swing.JList;
import javax.swing.border.Border;
+import docking.util.image.ToolIconURL;
import docking.widgets.list.GListCellRenderer;
-import ghidra.framework.project.tool.ToolIconURL;
class ToolIconUrlRenderer extends GListCellRenderer {
private Border emptyBorder = BorderFactory.createEmptyBorder(5, 5, 5, 5);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
index 47e80e0834..49beb9b2b1 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
@@ -21,6 +21,7 @@ import javax.swing.ImageIcon;
import org.jdom.Element;
+import docking.util.image.ToolIconURL;
import ghidra.framework.model.*;
import ghidra.util.Msg;
import ghidra.util.NumericUtilities;
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/test/DummyTool.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyTool.java
similarity index 98%
rename from Ghidra/Features/Base/src/test/java/ghidra/test/DummyTool.java
rename to Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyTool.java
index fb1076ca11..32c00a05ca 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/test/DummyTool.java
+++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyTool.java
@@ -30,10 +30,10 @@ import docking.*;
import docking.action.DockingActionIf;
import docking.actions.DockingToolActions;
import docking.actions.PopupActionProvider;
+import docking.util.image.ToolIconURL;
import ghidra.framework.model.*;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginEvent;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.program.model.listing.Program;
public class DummyTool implements Tool {
@@ -361,8 +361,7 @@ public class DummyTool implements Tool {
@Override
public void setMenuGroup(String[] menuPath, String group, String menuSubGroup) {
- // TODO Auto-generated method stub
-
+ //do nothing
}
@Override
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/test/DummyToolActions.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyToolActions.java
similarity index 100%
rename from Ghidra/Features/Base/src/test/java/ghidra/test/DummyToolActions.java
rename to Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyToolActions.java
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/test/DummyToolTemplate.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyToolTemplate.java
similarity index 96%
rename from Ghidra/Features/Base/src/test/java/ghidra/test/DummyToolTemplate.java
rename to Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyToolTemplate.java
index bf25e9daf7..75612afaea 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/test/DummyToolTemplate.java
+++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/test/DummyToolTemplate.java
@@ -19,8 +19,8 @@ import javax.swing.ImageIcon;
import org.jdom.Element;
+import docking.util.image.ToolIconURL;
import ghidra.framework.model.*;
-import ghidra.framework.project.tool.ToolIconURL;
import ghidra.program.model.listing.Program;
public class DummyToolTemplate implements ToolTemplate {
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/cmd/function/CreateFunctionThunkTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/cmd/function/CreateFunctionThunkTest.java
new file mode 100644
index 0000000000..17f1774aff
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/cmd/function/CreateFunctionThunkTest.java
@@ -0,0 +1,131 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.cmd.function;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+import ghidra.app.plugin.core.analysis.AnalysisBackgroundCommand;
+import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
+import ghidra.framework.cmd.Command;
+import ghidra.framework.options.Options;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.model.data.DataType;
+import ghidra.program.model.listing.Function;
+import ghidra.program.model.listing.Program;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+
+public class CreateFunctionThunkTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private TestEnv env;
+ private PluginTool tool;
+
+ private Program program;
+
+ private ProgramBuilder builder;
+
+ @Before
+ public void setUp() throws Exception {
+ env = new TestEnv();
+ tool = env.getTool();
+ }
+
+ @After
+ public void tearDown() {
+ if (program != null) {
+ env.release(program);
+ }
+ program = null;
+ env.dispose();
+ }
+
+ private void analyze() {
+ // turn off some analyzers
+ setAnalysisOptions("Stack");
+ setAnalysisOptions("Embedded Media");
+ setAnalysisOptions("DWARF");
+ setAnalysisOptions("Create Address Tables");
+ setAnalysisOptions("MIPS Constant Reference Analyzer");
+
+ AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(program);
+ analysisMgr.reAnalyzeAll(null);
+
+ Command cmd = new AnalysisBackgroundCommand(analysisMgr, false);
+ tool.execute(cmd, program);
+ waitForBusyTool(tool);
+ }
+
+ protected void setAnalysisOptions(String optionName) {
+ int txId = program.startTransaction("Analyze");
+ Options analysisOptions = program.getOptions(Program.ANALYSIS_PROPERTIES);
+ analysisOptions.setBoolean(optionName, false);
+ program.endTransaction(txId, true);
+ }
+
+ @Test
+ public void testDelaySlotThunk() throws Exception {
+
+ builder = new ProgramBuilder("thunk", ProgramBuilder._MIPS);
+
+ builder.setBytes("0x1000", "08 22 96 44 24 04 00 02 08 11 96 44 00 00 00 00");
+ builder.disassemble("0x1000", 27, false);
+ builder.disassemble("0x1008", 27, false);
+ builder.createFunction("0x1000");
+ builder.createFunction("0x1008");
+
+ builder.analyze();
+
+ program = builder.getProgram();
+
+ Function noThunk = program.getFunctionManager().getFunctionAt(builder.addr(0x1000));
+ assertEquals(false, noThunk.isThunk());
+
+ Function isThunk = program.getFunctionManager().getFunctionAt(builder.addr(0x1008));
+ assertEquals(true, isThunk.isThunk());
+ }
+
+ /**
+ * This tests the forcing of a function to be a thunk with CreateThunkFunctionCmd
+ * Tests that the Function start analyzer will create a thunk given the thunk tag on a matching function
+ * That the MIPS BE language has a thunking pattern.
+ * That the MIPS 64/32 hybrid with sign extension of registers still gets found as a thunk.
+ * That the thunking function can be found with out the constant reference analyzer
+ *
+ */
+ @Test
+ public void testDelayMips6432SlotThunk() throws Exception {
+
+ builder = new ProgramBuilder("thunk", ProgramBuilder._MIPS_6432);
+
+ builder.setBytes("0x466050", "3c 0f 00 47 8d f9 72 24 03 20 00 08 25 f8 72 24");
+ builder.setBytes("0x477224", "00 47 99 c0");
+ builder.createEmptyFunction("chdir", "0x4799c0", 1, DataType.VOID);
+ builder.disassemble("0x466050", 27, true);
+
+ builder.createFunction("0x466050");
+
+ program = builder.getProgram();
+
+ analyze();
+
+ Function isThunk = program.getFunctionManager().getFunctionAt(builder.addr(0x466050));
+ assertEquals(true, isThunk.isThunk());
+ assertEquals("chdir", isThunk.getName());
+ }
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/ClipboardPluginTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/ClipboardPluginTest.java
new file mode 100644
index 0000000000..7fb6ca334f
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/clipboard/ClipboardPluginTest.java
@@ -0,0 +1,1736 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.clipboard;
+
+import static org.junit.Assert.*;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.awt.event.FocusListener;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import org.junit.*;
+
+import docking.*;
+import docking.action.*;
+import docking.dnd.GClipboard;
+import docking.widgets.OptionDialog;
+import docking.widgets.fieldpanel.FieldPanel;
+import docking.widgets.fieldpanel.support.FieldSelection;
+import generic.test.TestUtils;
+import ghidra.app.cmd.label.RenameLabelCmd;
+import ghidra.app.events.ProgramSelectionPluginEvent;
+import ghidra.app.plugin.core.byteviewer.*;
+import ghidra.app.plugin.core.clear.ClearCmd;
+import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
+import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
+import ghidra.app.plugin.core.decompile.*;
+import ghidra.app.plugin.core.format.ByteBlockSelection;
+import ghidra.app.plugin.core.format.DataFormatModel;
+import ghidra.app.plugin.core.functiongraph.FGClipboardProvider;
+import ghidra.app.services.ClipboardContentProviderService;
+import ghidra.app.util.ByteCopier;
+import ghidra.app.util.viewer.field.LabelFieldFactory;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.model.address.*;
+import ghidra.program.model.data.DataType;
+import ghidra.program.model.listing.*;
+import ghidra.program.model.mem.Memory;
+import ghidra.program.model.mem.MemoryAccessException;
+import ghidra.program.model.symbol.*;
+import ghidra.program.util.*;
+import ghidra.test.*;
+import ghidra.util.Msg;
+
+/**
+ *
+ * Note: This test is sensitive to focus. So, don't click any windows while this test
+ * is running.
+ *
+ */
+
+public class ClipboardPluginTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private static final String COPY_ACTION_NAME = "Copy";
+ private static final String PASTE_ACTION_NAME = "Paste";
+ private static final String COPY_SPECIAL_ACTION_NAME = "Copy Special";
+ private static final Transferable DUMMY_TRANSFERABLE = new DummyTransferable();
+
+ private PluginTool tool;
+ private TestEnv env;
+ private Program program;
+ private Map> actionMap;
+ private ByteViewerClipboardProvider byteViewerClipboardProvider;
+ private DecompilerClipboardProvider decompileClipboardProvider;
+ private CodeBrowserClipboardProvider codeBrowserClipboardProvider;
+ private CodeBrowserPlugin codeBrowserPlugin;
+ private ByteViewerPlugin byteViewerPlugin;
+ private DecompilePlugin decompilePlugin;
+ private CodeViewerProvider codeViewerProvider;
+ private ClipboardPlugin clipboardPlugin;
+ private ComponentProvider decompileProvider;
+ private ComponentProviderWrapper codeViewerWrapper;
+ private ComponentProviderWrapper byteViewerWrapper;
+ private ComponentProviderWrapper decompilerWrapper;
+
+ @Before
+ public void setUp() throws Exception {
+
+ String name = super.testName.getMethodName();
+ if (name.endsWith("_Notepad")) {
+ program = createNotepadProgram();
+ }
+ else {
+ program = createDefaultProgram();
+ }
+
+ env = new TestEnv();
+ setErrorGUIEnabled(false);
+ tool = env.launchDefaultTool(program);
+
+ waitForBusyTool(tool);
+ waitForSwing();
+
+ // get actions for each service provider
+ clipboardPlugin = getPlugin(tool, ClipboardPlugin.class);
+ actionMap = getActionMap();
+
+ // Showing decompiler establishes clipboard provider for it
+ initializeComponentProviders();
+
+ initializeClipboardProviders();
+
+ clearClipboardContents();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ env.dispose();
+ }
+
+ private Program createDefaultProgram() throws Exception {
+ ProgramBuilder builder = new ProgramBuilder("default", ProgramBuilder._TOY, this);
+
+ builder.createMemory("test", "0x01001050", 20000);
+
+ builder.setBytes("0x01001050",
+ "0e 5e f4 77 33 58 f4 77 91 45 f4 77 88 7c f4 77 8d 70 f5 77 05 62 f4 77 f0 a3 " +
+ "f4 77 09 56 f4 77 10 17 f4 77 f7 29 f6 77 02 59 f4 77");
+
+ builder.createMemoryReference("0x01002cc0", "0x01002cf0", RefType.DATA,
+ SourceType.USER_DEFINED);
+ builder.createMemoryReference("0x01002d04", "0x01002d0f", RefType.DATA,
+ SourceType.USER_DEFINED);
+
+ DataType dt = DataType.DEFAULT;
+ Parameter p = new ParameterImpl(null, dt, builder.getProgram());
+ builder.createEmptyFunction("ghidra", "0x01002cf5", 1, dt, p);
+ builder.createEmptyFunction("sscanf", "0x0100415a", 1, dt, p);
+
+ builder.createComment("0x0100415a",
+ "\n;|||||||||||||||||||| FUNCTION ||||||||||||||||||||||||||||||||||||||||||||||||||\n ",
+ CodeUnit.PLATE_COMMENT);
+
+ builder.setBytes("0x0100418c", "ff 15 08 10 00 01");
+ builder.disassemble("0x0100418c", 6);
+
+ return builder.getProgram();
+ }
+
+ private Program createNotepadProgram() throws Exception {
+ ClassicSampleX86ProgramBuilder builder =
+ new ClassicSampleX86ProgramBuilder("notepad", false, this);
+
+ // need a default label at 01002cf0, so make up a reference
+ builder.createMemoryReference("01002ce5", "01002cf0", RefType.FALL_THROUGH,
+ SourceType.ANALYSIS);
+
+ return builder.getProgram();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map> getActionMap() {
+ return (Map>) getInstanceField(
+ "serviceActionMap", clipboardPlugin);
+ }
+
+ private void initializeClipboardProviders() {
+ Map> serviceMap = getActionMap();
+ Set services = serviceMap.keySet();
+ for (Object object : services) {
+ if (object instanceof ByteViewerClipboardProvider) {
+ byteViewerClipboardProvider = (ByteViewerClipboardProvider) object;
+ }
+ else if (object instanceof CodeBrowserClipboardProvider &&
+ !(object instanceof FGClipboardProvider)) {
+ codeBrowserClipboardProvider = (CodeBrowserClipboardProvider) object;
+ }
+ else if (object instanceof DecompilerClipboardProvider) {
+ decompileClipboardProvider = (DecompilerClipboardProvider) object;
+ }
+ }
+ }
+
+ private void initializeComponentProviders() {
+ codeBrowserPlugin = getPlugin(tool, CodeBrowserPlugin.class);
+ codeViewerProvider =
+ (CodeViewerProvider) invokeInstanceMethod("getProvider", codeBrowserPlugin);
+ codeViewerWrapper = new CodeViewerWrapper(codeViewerProvider);
+ tool.showComponentProvider(codeViewerProvider, true);
+
+ decompilePlugin = getPlugin(tool, DecompilePlugin.class);
+ ComponentProvider decompiler = tool.getComponentProvider("Decompiler");
+ tool.showComponentProvider(decompiler, true);
+
+ waitForSwing();
+ byteViewerPlugin = getPlugin(tool, ByteViewerPlugin.class);
+ ComponentProvider provider =
+ (ComponentProvider) getInstanceField("connectedProvider", byteViewerPlugin);
+ byteViewerWrapper = new ByteViewerWrapper(provider);
+ tool.showComponentProvider(provider, true);
+ setByteViewerEditable(false);
+
+ decompileProvider = waitForComponentProvider(DecompilerProvider.class);
+ decompilerWrapper = new DecompilerWrapper(decompileProvider);
+
+ waitForSwing();
+ }
+
+ private DockingAction getAction(ClipboardContentProviderService service, String actionName) {
+ List list = actionMap.get(service);
+ for (DockingAction pluginAction : list) {
+ if (pluginAction.getName().equals(actionName)) {
+ return pluginAction;
+ }
+ }
+ return null;
+ }
+
+ private Address addr(String address) {
+ AddressFactory addressFactory = program.getAddressFactory();
+ return addressFactory.getAddress(address);
+ }
+
+ private void renameLabel(ProgramLocation location, String oldName, String newName) {
+ RenameLabelCmd cmd =
+ new RenameLabelCmd(location.getAddress(), oldName, newName, SourceType.USER_DEFINED);
+ assertTrue(applyCmd(program, cmd));
+ SymbolTable symbolTable = program.getSymbolTable();
+ Symbol currentSymbol = symbolTable.getPrimarySymbol(location.getAddress());
+ assertEquals(newName, currentSymbol.getName());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromLabel_PasteToLabel() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from one label and then
+ // paste onto another label. This is useful for copying across programs, as doing so
+ // in the same program will fail due to a name collision. So, we have to perform the
+ // following steps to test the code:
+ // 1) Name a label to a custom name
+ // 2) Put the cursor on that label and execute the copy command (to get the name on the clipboard)
+ // 3) Perform an undo to remove the custom name we created
+ // 4) Put the cursor on a different label and execute the paste command
+ //
+
+ String oldLabelName = "DAT_01002cf0";
+ LabelFieldLocation location =
+ new LabelFieldLocation(program, addr("01002cf0"), oldLabelName, null, 0);
+ codeBrowserPlugin.goTo(location);
+
+ // 1)
+ String newLabelName = "BigBuddyBob";
+ renameLabel(location, oldLabelName, newLabelName);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ location = (LabelFieldLocation) currentLocation;
+ assertTrue(newLabelName.equals(location.getName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ undo(program);
+
+ // 4)
+ LabelFieldLocation newLabelLocation =
+ new LabelFieldLocation(program, addr("01002d0f"), "DAT_01002d0f", null, 0);
+ codeBrowserPlugin.goTo(newLabelLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ location = (LabelFieldLocation) currentLocation;
+ Msg.debug(this, "checking name!: " + location);
+ assertEquals(newLabelName, location.getName());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromLabel_PasteToFunction() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from one label and then
+ // paste onto a function. This is useful for copying across programs, as doing so
+ // in the same program will fail due to a name collision. So, we have to perform the
+ // following steps to test the code:
+ // 1) Name a label to a custom name
+ // 2) Put the cursor on that label and execute the copy command (to get the name on the clipboard)
+ // 3) Perform an undo to remove the custom name we created
+ // 4) Put the cursor on a different label and execute the paste command
+ //
+
+ String oldLabelName = "DAT_01002cf0";
+ LabelFieldLocation location =
+ new LabelFieldLocation(program, addr("01002cf0"), oldLabelName, null, 0);
+ codeBrowserPlugin.goTo(location);
+
+ // 1)
+ String newLabelName = "BigBuddyBob";
+ renameLabel(location, oldLabelName, newLabelName);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ location = (LabelFieldLocation) currentLocation;
+ assertTrue(newLabelName.equals(location.getName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ undo(program);
+
+ // 4)
+ FunctionNameFieldLocation functionLocation =
+ new FunctionNameFieldLocation(program, addr("01002cf5"), "ghidra");
+ codeBrowserPlugin.goTo(functionLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof FunctionNameFieldLocation);
+ functionLocation = (FunctionNameFieldLocation) currentLocation;
+ assertEquals(newLabelName, functionLocation.getFunctionName());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromLabel_PasteToComment() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from a label and then
+ // paste onto a comment.
+ //
+
+ String oldLabelName = "DAT_01002cf0";
+ LabelFieldLocation location =
+ new LabelFieldLocation(program, addr("01002cf0"), oldLabelName, null, 0);
+ codeBrowserPlugin.goTo(location);
+
+ // 1)
+ String newLabelName = "BigBuddyBob";
+ renameLabel(location, oldLabelName, newLabelName);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ location = (LabelFieldLocation) currentLocation;
+ assertTrue(newLabelName.equals(location.getName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ PlateFieldLocation plateFieldLocation =
+ new PlateFieldLocation(program, addr("0100415a"), null, 1, 10, new String[] { "" }, 0);
+
+ codeBrowserPlugin.goTo(plateFieldLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof PlateFieldLocation);
+ plateFieldLocation = (PlateFieldLocation) currentLocation;
+ String[] comments = plateFieldLocation.getComment();
+ assertEquals(1, comments.length);
+ assertEquals(newLabelName, comments[0]);
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromLabel_PasteToVariable_Notepad() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from one label and then
+ // paste onto a variable. This is useful for copying across programs, as doing so
+ // in the same program will fail due to a name collision.
+ //
+ // So, we have to perform the following steps to test the code:
+ // 1) Name a label to a custom name
+ // 2) Put the cursor on that label and execute the copy command (to get the name on the clipboard)
+ // 3) Perform an undo to remove the custom name we created
+ // 4) Put the cursor on a different label and execute the paste command
+ //
+
+ String oldLabelName = "LAB_01002cf0";
+ LabelFieldLocation location =
+ new LabelFieldLocation(program, addr("01002cf0"), oldLabelName, null, 0);
+ codeBrowserPlugin.goTo(location);
+
+ // 1)
+ String newLabelName = "BigBuddyBob";
+ renameLabel(location, oldLabelName, newLabelName);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ location = (LabelFieldLocation) currentLocation;
+ assertTrue(newLabelName.equals(location.getName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ undo(program);
+
+ // 4)
+ String operandPrefix = "dword ptr [EBP + ";
+ String operandReferenceName = "destStr]";
+ OperandFieldLocation variableOperandReferenceLocation = new OperandFieldLocation(program,
+ addr("0100416c"), null, addr("0x8"), operandPrefix + operandReferenceName, 1, 9);
+ codeBrowserPlugin.goTo(variableOperandReferenceLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof OperandFieldLocation);
+ variableOperandReferenceLocation = (OperandFieldLocation) currentLocation;
+ assertEquals(operandPrefix + newLabelName + "]",
+ variableOperandReferenceLocation.getOperandRepresentation());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromOperandReference_PasteToLabel_Notepad() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from an operand reference
+ // label and then paste onto another label. This is useful for copying across
+ // programs, as doing so in the same program will fail due to a name
+ // collision.
+
+ // So, we have to perform the following steps to test the code:
+ // 1) Put the cursor on an operand and execute the copy command (to get the name on the clipboard)
+ // 2) Name that operand's label to a custom name
+ // 3) Put the cursor on a different label and execute the paste command
+ //
+
+ // 1)
+ String operandText = "dword ptr [->ADVAPI32.dll::RegQueryValueExW]";
+ OperandFieldLocation operandFieldLocation = new OperandFieldLocation(program,
+ addr("0100418c"), null, addr("01001008"), operandText, 15, 0);
+ codeBrowserPlugin.goTo(operandFieldLocation);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof OperandFieldLocation);
+ operandFieldLocation = (OperandFieldLocation) currentLocation;
+ assertEquals(operandText, operandFieldLocation.getOperandRepresentation());
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 2)
+ String oldLabelName = "ADVAPI32.dll_RegQueryValueExW";
+ LabelFieldLocation renameLabelLocation =
+ new LabelFieldLocation(program, addr("01001008"), oldLabelName, null, 0);
+ codeBrowserPlugin.goTo(renameLabelLocation);
+
+ String newLabelName = "BigBuddyBob";
+ renameLabel(renameLabelLocation, oldLabelName, newLabelName);
+
+ // 3)
+ String oldPasteName = "LAB_01002cf0";
+ LabelFieldLocation pasteLabelLocation =
+ new LabelFieldLocation(program, addr("01002cf0"), oldPasteName, null, 0);
+ assertTrue(
+ codeBrowserPlugin.goToField(addr("01002cf0"), LabelFieldFactory.FIELD_NAME, 0, 0));
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ pasteLabelLocation = (LabelFieldLocation) currentLocation;
+ assertEquals(oldLabelName, pasteLabelLocation.getName());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromFunction_PasteToLabel() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from one function label and then
+ // paste onto another label. This is useful for copying across programs, as doing so
+ // in the same program will fail due to a name collision. So, we have to perform the
+ // following steps to test the code:
+ // 1) Name a label to a custom name
+ // 2) Put the cursor on that label and execute the copy command (to get the name on the clipboard)
+ // 3) Perform an undo to remove the custom name we created
+ // 4) Put the cursor on a different label and execute the paste command
+ //
+
+ String functionName = "sscanf";
+ FunctionNameFieldLocation location =
+ new FunctionNameFieldLocation(program, addr("0100415a"), functionName);
+ codeBrowserPlugin.goTo(location);
+
+ // 1)
+ String newLabelName = "BigBuddyBob";
+ renameLabel(location, functionName, newLabelName);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof FunctionNameFieldLocation);
+ location = (FunctionNameFieldLocation) currentLocation;
+ assertTrue(newLabelName.equals(location.getFunctionName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+ assertEquals(newLabelName, getClipboardContents());
+
+ // 3)
+ undo(program);
+
+ // 4)
+ LabelFieldLocation newLabelLocation =
+ new LabelFieldLocation(program, addr("01002d0f"), "DAT_01002d0f", null, 0);
+ codeBrowserPlugin.goTo(newLabelLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof LabelFieldLocation);
+ LabelFieldLocation labelLocation = (LabelFieldLocation) currentLocation;
+ assertEquals(newLabelName, labelLocation.getName());
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromFunction_PasteToComment() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from a function and then
+ // paste onto a comment.
+ //
+
+ // 1)
+ String functionName = "sscanf";
+ FunctionNameFieldLocation location =
+ new FunctionNameFieldLocation(program, addr("0100415a"), functionName);
+ codeBrowserPlugin.goTo(location);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof FunctionNameFieldLocation);
+ location = (FunctionNameFieldLocation) currentLocation;
+ assertTrue(functionName.equals(location.getFunctionName()));
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ PlateFieldLocation plateFieldLocation =
+ new PlateFieldLocation(program, addr("0100415a"), null, 1, 10, new String[] { "" }, 0);
+
+ codeBrowserPlugin.goTo(plateFieldLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof PlateFieldLocation);
+ plateFieldLocation = (PlateFieldLocation) currentLocation;
+ String[] comments = plateFieldLocation.getComment();
+ assertEquals(1, comments.length);
+ assertEquals(functionName, comments[0]);
+ }
+
+ @Test
+ public void testCodeBrowser_CopyFromComment_PasteToComment() throws Exception {
+ //
+ // Test that we can copy (simulating the keyboard action) from one comment and then
+ // paste onto another comment.
+ //
+
+ PlateFieldLocation plateFieldLocation =
+ new PlateFieldLocation(program, addr("0100415a"), null, 1, 10, new String[] { "" }, 0);
+
+ codeBrowserPlugin.goTo(plateFieldLocation);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof PlateFieldLocation);
+ plateFieldLocation = (PlateFieldLocation) currentLocation;
+ String commentText = plateFieldLocation.getComment()[1];
+ assertEquals(
+ ";|||||||||||||||||||| FUNCTION ||||||||||||||||||||||||||||||||||||||||||||||||||",
+ commentText);
+
+ assertTrue(codeBrowserClipboardProvider.canCopy());
+
+ // 2)
+ DockingAction copyAction = getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ ActionContext context = codeViewerProvider.getActionContext(null);
+ performAction(copyAction, context, true);
+
+ // 3)
+ // 0100415a PreCommentFieldLocation
+ CommentFieldLocation commentFieldLocation = new CommentFieldLocation(program,
+ addr("0100415a"), null, new String[] { "" }, CodeUnit.PRE_COMMENT, 5, 10);
+
+ codeBrowserPlugin.goTo(plateFieldLocation);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ performAction(pasteAction, context, true);
+ waitForBusyTool(tool);
+
+ currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof CommentFieldLocation);
+ commentFieldLocation = (CommentFieldLocation) currentLocation;
+ String[] comments = commentFieldLocation.getComment();
+ assertEquals(2, comments.length);
+ assertEquals(commentText, comments[0]);
+ }
+
+ @Test
+ public void testCopyActionEnablement() {
+
+ // no copy on by default
+ DockingAction byteViewerCopyAction =
+ getAction(byteViewerClipboardProvider, COPY_ACTION_NAME);
+ DockingAction codeBrowserCopyAction =
+ getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ DockingAction decompileCopyAction = getAction(decompileClipboardProvider, COPY_ACTION_NAME);
+
+ waitForSwing();
+
+ assertTrue(!byteViewerCopyAction.isEnabled());
+ assertTrue(!codeBrowserCopyAction.isEnabled());
+
+ // this action is enabled on any text
+ // assertTrue(!decompileCopyAction.isEnabled());
+
+ // give the providers focus and check their actions state
+ assertTrue(!byteViewerCopyAction.isEnabled());
+
+ // the code browser is special--make sure that it not only has focus, but that the location
+ // of the cursor is not one of the 'special' copy locations
+ codeBrowserPlugin.goTo(new MnemonicFieldLocation(program, addr("1001050")));
+ assertTrue(codeBrowserCopyAction.isEnabled());
+
+ // For each provider:
+ // check copy on selection state
+ makeSelection(byteViewerWrapper);
+
+ // check the state
+ assertTrue(byteViewerCopyAction.isEnabled());
+
+ // clear the selection
+ byteViewerWrapper.clearSelection();
+
+ // check copy on selection state
+ makeSelection(codeViewerWrapper);
+
+ // check the state
+ boolean enabled = codeBrowserCopyAction.isEnabled();
+ assertTrue(enabled);
+
+ // clear the selection
+ codeViewerWrapper.clearSelection();
+
+ // check copy on selection state
+ makeSelection(decompilerWrapper);
+
+ // check the state
+ assertTrue(decompileCopyAction.isEnabled());
+ }
+
+ @Test
+ public void testPasteActionEnablement() {
+
+ DockingAction byteViewerCopyAction =
+ getAction(byteViewerClipboardProvider, COPY_ACTION_NAME);
+ DockingAction codeBrowserCopyAction =
+ getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ DockingAction decompileCopyAction = getAction(decompileClipboardProvider, COPY_ACTION_NAME);
+
+ // no paste on by default
+ DockingAction byteViewerPasteAction =
+ getAction(byteViewerClipboardProvider, PASTE_ACTION_NAME);
+ DockingAction codeBrowserPasteAction =
+ getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+
+ assertTrue(!byteViewerPasteAction.isEnabled());
+ assertTrue(!codeBrowserPasteAction.isEnabled());
+
+ // For each provider make sure no paste on a selection without a copy:
+ // check copy on selection state
+ makeSelection(byteViewerWrapper);
+
+ // check the state
+ assertTrue(!byteViewerPasteAction.isEnabled());
+
+ // clear the selection
+ byteViewerWrapper.clearSelection();
+
+ // check copy on selection state
+ codeViewerWrapper.clearSelection();
+
+ // check the state
+ assertTrue(!codeBrowserPasteAction.isEnabled());
+
+ // clear the selection
+ codeViewerWrapper.clearSelection();
+
+ // check copy on selection state
+ makeSelection(decompilerWrapper);
+
+ // clear the selection
+ decompilerWrapper.clearSelection();
+
+ // For each provider make sure paste on a selection after a copy:
+ // check paste on selection state
+ makeSelection(byteViewerWrapper);
+ copy(byteViewerWrapper, byteViewerCopyAction);
+
+ // check the state
+ assertTrue(!byteViewerPasteAction.isEnabled());
+ setByteViewerEditable(true);
+ assertTrue(byteViewerPasteAction.isEnabled());
+
+ // clear the selection and clipboard
+ byteViewerWrapper.clearSelection();
+ clearClipboardContents();
+
+ makeSelection(codeViewerWrapper);
+
+ copy(codeViewerWrapper, codeBrowserCopyAction);
+
+// TODO: if we ever find a way to prevent the paste action from being enabled without
+// actually doing the paste, then the following line of code can be put back in the test
+// check the state - can't paste a generic copy!
+//assertTrue( !codeBrowserPasteAction.isEnabled() );
+
+ // now perform a 'copy special' and choose a type that the paste will accept
+ DockingAction codeBrowserCopySpecialAction =
+ getAction(codeBrowserClipboardProvider, COPY_SPECIAL_ACTION_NAME);
+ assertTrue(codeBrowserCopySpecialAction.isEnabled());
+ copySpecial(codeViewerWrapper, codeBrowserCopySpecialAction);
+
+ assertTrue(codeBrowserPasteAction.isEnabled());
+
+ // clear the selection and clipboard
+ codeViewerWrapper.clearSelection();
+ clearClipboardContents();
+
+ // check copy on selection state
+ makeSelection(decompilerWrapper);
+ copy(decompilerWrapper, decompileCopyAction);
+
+ // clear the selection and clipboard
+ decompilerWrapper.clearSelection();
+ clearClipboardContents();
+ }
+
+ @Test
+ public void testCopyPasteAcrossServiceProviders() {
+ DockingAction byteViewerCopyAction =
+ getAction(byteViewerClipboardProvider, COPY_ACTION_NAME);
+ DockingAction codeBrowserCopyAction =
+ getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ DockingAction decompileCopyAction = getAction(decompileClipboardProvider, COPY_ACTION_NAME);
+
+ DockingAction byteViewerPasteAction =
+ getAction(byteViewerClipboardProvider, PASTE_ACTION_NAME);
+ DockingAction codeBrowserPasteAction =
+ getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+
+ // byte viewer to code browser and decompiler
+ makeSelection(byteViewerWrapper);
+ copy(byteViewerWrapper, byteViewerCopyAction);
+
+ assertTrue(!byteViewerPasteAction.isEnabled());
+ setByteViewerEditable(true);
+ assertTrue(byteViewerPasteAction.isEnabled());
+ assertTrue(codeBrowserPasteAction.isEnabled());
+
+ codeViewerWrapper.clearSelection();
+ clearClipboardContents();
+
+ // sanity check
+ assertTrue(!byteViewerPasteAction.isEnabled());
+ assertTrue(!codeBrowserPasteAction.isEnabled());
+
+ // code browser to byte viewer and decompiler
+ makeSelection(codeViewerWrapper);
+ copy(codeViewerWrapper, codeBrowserCopyAction);
+
+// TODO: if we ever find a way to prevent the paste action from being enabled without
+// actually doing the paste, then the following code can be put back in the test
+//assertTrue( !byteViewerPasteAction.isEnabled() );
+//assertTrue( !codeBrowserPasteAction.isEnabled() );
+
+ // now perform a 'copy special' and choose a type that the paste will accept
+ DockingAction codeBrowserCopySpecialAction =
+ getAction(codeBrowserClipboardProvider, COPY_SPECIAL_ACTION_NAME);
+ copySpecial(codeViewerWrapper, codeBrowserCopySpecialAction);
+
+ assertTrue(byteViewerPasteAction.isEnabled());
+ assertTrue(codeBrowserPasteAction.isEnabled());
+
+ // change the edit state of the byte viewer and make sure we can paste
+ setByteViewerEditable(true);
+
+ assertTrue(byteViewerPasteAction.isEnabled());
+
+ // clear the selection and clipboard
+ codeViewerWrapper.clearSelection();
+ clearClipboardContents();
+
+ // sanity check
+ assertTrue(!byteViewerPasteAction.isEnabled());
+ assertTrue(!codeBrowserPasteAction.isEnabled());
+
+ // copy from decompiler can not paste into the other two
+ makeSelection(decompilerWrapper);
+ copy(decompilerWrapper, decompileCopyAction);
+
+// TODO: if we ever find a way to prevent the paste action from being enabled without
+// actually doing the paste, then the following code can be put back in the test
+//assertTrue( !byteViewerPasteAction.isEnabled() );
+//assertTrue( !codeBrowserPasteAction.isEnabled() );
+ }
+
+ @Test
+ public void testCopyPasteAcrossServiceProviders_CodeBrowser_To_ByteViewer() throws Exception {
+ DockingAction codeBrowserCopyAction =
+ getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ final DockingAction byteViewerPasteAction =
+ getAction(byteViewerClipboardProvider, PASTE_ACTION_NAME);
+
+ // code browser to byte viewer and decompiler
+ makeSelection(codeViewerWrapper);
+ copy(codeViewerWrapper, codeBrowserCopyAction);
+
+ // now perform a 'copy special' and choose a type that the paste will accept
+ DockingAction codeBrowserCopySpecialAction =
+ getAction(codeBrowserClipboardProvider, COPY_SPECIAL_ACTION_NAME);
+ copySpecial(codeViewerWrapper, codeBrowserCopySpecialAction);
+
+ String clipboardContents = getClipboardContents();
+
+ // move the cursor down to paste our bytes; clear the code we need to be able to paste
+ ProgramSelection currentSelection = codeBrowserPlugin.getCurrentSelection();
+ Address pasteAddress = currentSelection.getMaxAddress();
+ long pasteLength = currentSelection.getNumAddresses();
+ clearSelectedBytes(pasteAddress, pasteLength);
+
+ codeBrowserPlugin.goTo(new ProgramLocation(program, pasteAddress));
+
+ // change the edit state of the byte viewer and make sure we can paste
+ setByteViewerEditable(true);
+
+ assertTrue(byteViewerPasteAction.isEnabled());
+
+ runSwing(() -> byteViewerPasteAction.actionPerformed(getActionContext(byteViewerWrapper)),
+ false);
+
+ OptionDialog d = waitForDialogComponent(OptionDialog.class);
+ pressButton(d.getDefaultButton());
+
+ waitForBusyTool(tool);
+
+ assertByteViewerBytes(clipboardContents, pasteAddress);
+ }
+
+ @Test
+ public void testCopyPasteAcrossServiceProviders_CodeBrowser_To_ByteViewer_BytesWithNoSpaces()
+ throws Exception {
+ DockingAction codeBrowserCopyAction =
+ getAction(codeBrowserClipboardProvider, COPY_ACTION_NAME);
+ final DockingAction byteViewerPasteAction =
+ getAction(byteViewerClipboardProvider, PASTE_ACTION_NAME);
+
+ // code browser to byte viewer and decompiler
+ makeSelection(codeViewerWrapper);
+ copy(codeViewerWrapper, codeBrowserCopyAction);
+
+ // now perform a 'copy special' and choose a type that the paste will accept
+ DockingAction codeBrowserCopySpecialAction =
+ getAction(codeBrowserClipboardProvider, COPY_SPECIAL_ACTION_NAME);
+ copySpecial_ByteStringNoSpaces(codeViewerWrapper, codeBrowserCopySpecialAction);
+
+ String clipboardContents = getClipboardContents();
+
+ // move the cursor down to paste our bytes; clear the code we need to be able to paste
+ ProgramSelection currentSelection = codeBrowserPlugin.getCurrentSelection();
+ Address pasteAddress = currentSelection.getMaxAddress();
+ long pasteLength = currentSelection.getNumAddresses();
+ clearSelectedBytes(pasteAddress, pasteLength);
+
+ codeBrowserPlugin.goTo(new ProgramLocation(program, pasteAddress));
+
+ // change the edit state of the byte viewer and make sure we can paste
+ setByteViewerEditable(true);
+
+ assertTrue(byteViewerPasteAction.isEnabled());
+
+ runSwing(() -> byteViewerPasteAction.actionPerformed(getActionContext(byteViewerWrapper)),
+ false);
+
+ OptionDialog d = waitForDialogComponent(OptionDialog.class);
+ pressButton(d.getDefaultButton());
+
+ waitForBusyTool(tool);
+
+ assertByteViewerBytes(clipboardContents, pasteAddress);
+ }
+
+ @Test
+ public void testCopyTextSelection_ByteViewer_SingleLayout() throws Exception {
+
+ ByteViewerOptionsDialog dialog = launchByteViewerOptions();
+ setByteViewerViewSelected(dialog, "Ascii", true);
+ setByteViewerViewSelected(dialog, "Octal", true);
+ pressButtonByText(dialog.getComponent(), "OK");
+
+ ByteViewerPlugin plugin = env.getPlugin(ByteViewerPlugin.class);
+ ProgramByteViewerComponentProvider provider = plugin.getProvider();
+ ByteViewerPanel panel =
+ (ByteViewerPanel) invokeInstanceMethod("getByteViewerPanel", provider);
+
+ Window window = windowForComponent(panel);
+ assertNotNull(window);
+ Dimension size = window.getSize();
+ window.setSize(1000, size.height);// resize so that we can click the various views
+
+ //
+ // Test copying from the Hex view
+ //
+ ByteViewerComponent bc = findByteViewerComponent(panel, "Hex");
+ assertTrue(bc.isVisible());
+
+ Rectangle bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ ProgramSelection selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001050"), addr("1001052"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ final DockingAction byteViewerCopyAction =
+ getAction(byteViewerClipboardProvider, COPY_ACTION_NAME);
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ Object data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals("0e 5e f4", data);
+
+ //
+ // Test copying from the Hex view
+ //
+ bc = findByteViewerComponent(panel, "Ascii");
+ assertTrue(bc.isVisible());
+
+ bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001050"), addr("1001052"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ systemClipboard = GClipboard.getSystemClipboard();
+ contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals(". ^ .", data);
+
+ //
+ // Test copying from the Octal view
+ //
+ bc = findByteViewerComponent(panel, "Octal");
+ assertTrue(bc.isVisible());
+
+ bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001050"), addr("1001052"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ systemClipboard = GClipboard.getSystemClipboard();
+ contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals("016 136 364", data);
+ }
+
+ @Test
+ public void testCopyTextSelection_ByteViewer_MultipleLayout() throws Exception {
+ ByteViewerOptionsDialog dialog = launchByteViewerOptions();
+ setByteViewerViewSelected(dialog, "Ascii", true);
+ setByteViewerViewSelected(dialog, "Octal", true);
+ pressButtonByText(dialog.getComponent(), "OK");
+
+ ByteViewerPlugin plugin = env.getPlugin(ByteViewerPlugin.class);
+ ProgramByteViewerComponentProvider provider = plugin.getProvider();
+ ByteViewerPanel panel =
+ (ByteViewerPanel) invokeInstanceMethod("getByteViewerPanel", provider);
+
+ Window window = windowForComponent(panel);
+ assertNotNull(window);
+ Dimension size = window.getSize();
+ window.setSize(1000, size.height);// resize so that we can click the various views
+
+ //
+ // Test copying from the Hex view
+ //
+ ByteViewerComponent bc = findByteViewerComponent(panel, "Hex");
+ assertTrue(bc.isVisible());
+
+ Rectangle bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ ProgramSelection selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001051"), addr("1001070"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ final DockingAction byteViewerCopyAction =
+ getAction(byteViewerClipboardProvider, COPY_ACTION_NAME);
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ Object data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals("5e f4 77 33 58 f4 77 91 45 f4 77 88 7c f4 77 8d 70 f5 77 05 " +
+ "62 f4 77 f0 a3 f4 77 09 56 f4 77 10", data);
+
+ //
+ // Test copying from the Hex view
+ //
+ bc = findByteViewerComponent(panel, "Ascii");
+ assertTrue(bc.isVisible());
+
+ bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001051"), addr("1001070"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ systemClipboard = GClipboard.getSystemClipboard();
+ contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals("^ . w 3 X . w . E . w . | . w . p . w . b . w . . . w . V . w .", data);
+
+ //
+ // Test copying from the Octal view
+ //
+ bc = findByteViewerComponent(panel, "Octal");
+ assertTrue(bc.isVisible());
+
+ bounds = bc.getBounds();
+ clickMouse(bc, 1, bounds.x + 20, bounds.y + 20, 1, 0);
+
+ selection =
+ new ProgramSelection(program.getAddressFactory(), addr("1001051"), addr("1001070"));
+ tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
+
+ waitForBusyTool(tool);
+
+ runSwing(() -> byteViewerCopyAction.actionPerformed(getActionContext(byteViewerWrapper)));
+
+ systemClipboard = GClipboard.getSystemClipboard();
+ contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals("136 364 167 063 130 364 167 221 105 364 167 210 174 364 167 215 160 " +
+ "365 167 005 142 364 167 360 243 364 167 011 126 364 167 020", data);
+ }
+
+ @Test
+ public void testCodeBrowserPasteAsciiBytes() throws Exception {
+ //
+ // Test that we can paste characters from clipboard that contain some other special characters
+ // that are not normal ascii text and only the normal ascii text will get pasted.
+ // So, we have to perform the following steps to test the code:
+ // 1) Put ill formed characters into clipboard
+ // 2) Put the cursor on the code unit for pasting.
+ // 3) Paste the bytes
+ // 4) Verify the bytes don't contain the non-ascii text.
+ //
+
+ final char[] droppedChars =
+ new char[] { (char) 0x30, (char) 0x31, (char) 0xa0, (char) 0xc2, (char) 0x01,
+ (char) 0x63, (char) 0x30, (char) 0xa0, (char) 0x35, (char) 0x65, (char) 0x20 };
+// final byte[] pastedBytes =
+// new byte[] { (byte) 0x30, (byte) 0x31, (byte) 0x63, (byte) 0x30, (byte) 0x35,
+// (byte) 0x65 };
+ final byte[] resultBytes = new byte[] { (byte) 0x01, (byte) 0xc0, (byte) 0x5e };
+
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable transferable = new Transferable() {
+
+ @Override
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ if (flavor.equals(DataFlavor.stringFlavor)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[] { DataFlavor.stringFlavor };
+ }
+
+ @Override
+ public Object getTransferData(DataFlavor flavor)
+ throws UnsupportedFlavorException, IOException {
+ if (flavor.equals(DataFlavor.stringFlavor)) {
+ return new String(droppedChars);
+ }
+ return null;
+ }
+ };
+ // 1) Put ill formed characters into clipboard
+ systemClipboard.setContents(transferable, (clipboard, contents) -> {
+ // dummy listener so that we can be properly garbage collected
+ });
+ Transferable contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ Object data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals(new String(droppedChars), data);
+
+ AddressFieldLocation location = new AddressFieldLocation(program, addr("01002cf0"));
+ // 2) Put the cursor on the code unit for pasting.
+ codeBrowserPlugin.goTo(location);
+
+ ActionContext context = codeViewerProvider.getActionContext(null);
+
+ DockingAction pasteAction = getAction(codeBrowserClipboardProvider, PASTE_ACTION_NAME);
+ context = codeViewerProvider.getActionContext(null);
+ // 3) Paste the bytes
+ performAction(pasteAction, context, false);
+ OptionDialog d = waitForDialogComponent(OptionDialog.class);
+ pressButton(d.getDefaultButton());
+
+ waitForBusyTool(tool);
+
+ ProgramLocation currentLocation = codeBrowserPlugin.getCurrentLocation();
+ assertTrue(currentLocation instanceof AddressFieldLocation);
+ AddressFieldLocation addressLocation = (AddressFieldLocation) currentLocation;
+ assertEquals("01002cf0", addressLocation.getAddressRepresentation());
+ Address address = addressLocation.getAddress();
+ Memory memory = program.getMemory();
+ byte[] memoryBytes = new byte[resultBytes.length];
+ // 4) Verify the bytes don't contain the non-ascii text.
+ memory.getBytes(address, memoryBytes, 0, resultBytes.length);
+ assertTrue("The expected bytes were not pasted in the CodeBrowser.",
+ Arrays.equals(resultBytes, memoryBytes));
+ }
+
+ @Test
+ public void testByteViewerPasteAsciiBytes() throws Exception {
+ final DockingAction byteViewerPasteAction =
+ getAction(byteViewerClipboardProvider, PASTE_ACTION_NAME);
+ //
+ // Test that we can paste characters from clipboard that contain some other special characters
+ // that are not normal ascii text and only the normal ascii text will get pasted.
+ // So, we have to perform the following steps to test the code:
+ // 1) Put ill formed characters into clipboard
+ // 2) Put the cursor on the byte viewer address for pasting.
+ // 3) Paste the bytes
+ // 4) Verify the bytes don't contain the non-ascii text.
+ //
+
+ final char[] droppedChars =
+ new char[] { (char) 0x30, (char) 0x31, (char) 0xa0, (char) 0xc2, (char) 0x01,
+ (char) 0x63, (char) 0x30, (char) 0xa0, (char) 0x35, (char) 0x65, (char) 0x20 };
+// final byte[] pastedBytes =
+// new byte[] { (byte) 0x30, (byte) 0x31, (byte) 0x63, (byte) 0x30, (byte) 0x35,
+// (byte) 0x65 };
+ final byte[] resultBytes = new byte[] { (byte) 0x01, (byte) 0xc0, (byte) 0x5e };
+
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable transferable = new Transferable() {
+
+ @Override
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ if (flavor.equals(DataFlavor.stringFlavor)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[] { DataFlavor.stringFlavor };
+ }
+
+ @Override
+ public Object getTransferData(DataFlavor flavor)
+ throws UnsupportedFlavorException, IOException {
+ if (flavor.equals(DataFlavor.stringFlavor)) {
+ return new String(droppedChars);
+ }
+ return null;
+ }
+ };
+ // 1) Put ill formed characters into clipboard
+ systemClipboard.setContents(transferable, (clipboard, contents) -> {
+ // dummy listener so that we can be properly garbage collected
+ });
+ Transferable contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ Object data = contents.getTransferData(DataFlavor.stringFlavor);
+ assertEquals(new String(droppedChars), data);
+
+ Address address = program.getAddressFactory().getAddress("01002cf0");
+ AddressFieldLocation location = new AddressFieldLocation(program, address);
+ // 2) Put the cursor on the byte viewer address for pasting.
+ codeBrowserPlugin.goTo(location);
+
+ // change the edit state of the byte viewer and make sure we can paste
+ setByteViewerEditable(true);
+
+ assertTrue(byteViewerPasteAction.isEnabled());
+
+ // 3) Paste the bytes
+ runSwing(() -> byteViewerPasteAction.actionPerformed(getActionContext(byteViewerWrapper)),
+ false);
+
+ OptionDialog d = waitForDialogComponent(OptionDialog.class);
+ pressButton(d.getDefaultButton());
+
+ waitForSwing();
+
+ Memory memory = program.getMemory();
+ byte[] memoryBytes = new byte[resultBytes.length];
+ // 4) Verify the bytes don't contain the non-ascii text.
+ memory.getBytes(address, memoryBytes, 0, resultBytes.length);
+ assertTrue("The expected bytes were not pasted in the ByteViewer.",
+ Arrays.equals(resultBytes, memoryBytes));
+ }
+
+//==================================================================================================
+// Helper Methods
+//==================================================================================================
+
+ private ByteViewerOptionsDialog launchByteViewerOptions() {
+ Plugin plugin = env.getPlugin(ByteViewerPlugin.class);
+ final DockingActionIf action = getAction(plugin, "Byte Viewer Options");
+ assertTrue(action.isEnabled());
+
+ SwingUtilities.invokeLater(() -> action.actionPerformed(new ActionContext()));
+ waitForSwing();
+ ByteViewerOptionsDialog d = waitForDialogComponent(ByteViewerOptionsDialog.class);
+ return d;
+ }
+
+ private void setByteViewerViewSelected(ByteViewerOptionsDialog dialog, String viewName,
+ boolean selected) {
+ Map, ?> checkboxMap = (Map, ?>) getInstanceField("checkboxMap", dialog);
+ JCheckBox checkbox = (JCheckBox) checkboxMap.get(viewName);
+ checkbox.setSelected(selected);
+ }
+
+ private ByteViewerComponent findByteViewerComponent(Container container, String name) {
+ Component[] c = container.getComponents();
+ for (Component element : c) {
+ if (element instanceof ByteViewerComponent) {
+ DataFormatModel model =
+ (DataFormatModel) invokeInstanceMethod("getDataModel", element);
+ if (model.getName().equals(name)) {
+ return (ByteViewerComponent) element;
+ }
+ }
+ else if (element instanceof Container) {
+ ByteViewerComponent bvc = findByteViewerComponent((Container) element, name);
+ if (bvc != null) {
+ return bvc;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void setByteViewerEditable(boolean editable) {
+ ComponentProvider provider =
+ (ComponentProvider) getInstanceField("connectedProvider", byteViewerPlugin);
+
+ ToggleDockingActionIf action =
+ (ToggleDockingActionIf) getInstanceField("editModeAction", provider);
+
+ if (editable == action.isSelected()) {
+ return;
+ }
+
+ performAction(action, true);
+ }
+
+ private void makeSelection(ComponentProviderWrapper wrapper) {
+ ComponentProvider provider = wrapper.getComponentProvider();
+
+ wrapper.clearSelection();
+
+ Component component = provider.getComponent();
+
+ Point point = wrapper.getStartMouseDragLocation();
+ int startX = point.x;
+ int startY = point.y;
+
+ Point endPoint = wrapper.getEndMouseDragLocation();
+ int endX = endPoint.x;
+ int endY = endPoint.y;
+
+ final Component deepestComponent =
+ SwingUtilities.getDeepestComponentAt(component, startX, startY);
+ Msg.debug(this, "Preparing to make a selection on Java component \"" + deepestComponent +
+ "\" for test " + wrapper.getComponentProvider().getName());
+// Container parent = deepestComponent.getParent();
+// while ( parent != null ) {
+// Err.debug( this, "\twith parent: " + parent );
+// parent = parent.getParent();
+// }
+ dragMouse(deepestComponent, MouseEvent.BUTTON1, startX, startY, endX, endY, 0);
+ Msg.debug(this, "\tafter make selection");
+
+ wrapper.verifySelection();
+ }
+
+ private void clearClipboardContents() {
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ systemClipboard.setContents(DUMMY_TRANSFERABLE, null);
+ waitForSwing();
+
+ // something useful or snarky, it's up to me.
+ // make sure that the state is initialized properly, because the
+ // testing environment might have cruft in it (note, normal environment
+ // has cruft too) and this updates the stuff in the stuff
+ TestUtils.invokeInstanceMethod("updateCopyState", clipboardPlugin);
+ TestUtils.invokeInstanceMethod("updatePasteState", clipboardPlugin);
+ }
+
+ private String getClipboardContents() throws Exception {
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable transferable = systemClipboard.getContents(this);
+ return (String) transferable.getTransferData(DataFlavor.stringFlavor);
+ }
+
+ private void copy(ComponentProviderWrapper wrapper, final DockingAction copyAction) {
+ wrapper.verifySelection();
+
+ runSwing(() -> {
+ copyAction.actionPerformed(getActionContext(wrapper));
+ });
+
+ Clipboard systemClipboard = GClipboard.getSystemClipboard();
+ Transferable contents = systemClipboard.getContents(systemClipboard);
+ assertNotNull(contents);
+ assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
+ }
+
+ private ActionContext getActionContext(ComponentProviderWrapper wrapper) {
+ return runSwing(() -> {
+ ComponentProvider provider = wrapper.getComponentProvider();
+ ActionContext context = provider.getActionContext(null);
+ return context;
+ });
+ }
+
+ private void copySpecial(ComponentProviderWrapper wrapper, final DockingAction copyAction) {
+ wrapper.verifySelection();
+ executeOnSwingWithoutBlocking(() -> copyAction.actionPerformed(getActionContext(wrapper)));
+
+ // get the dialog and make a selection
+ JDialog dialog = waitForJDialog("Copy Special");
+ assertNotNull(dialog);
+ DockingDialog dockingDialog = (DockingDialog) dialog;
+ final DialogComponentProvider component =
+ (DialogComponentProvider) getInstanceField("component", dockingDialog);
+ Object listPanel = getInstanceField("listPanel", component);
+ final JList> list = (JList>) getInstanceField("list", listPanel);
+
+ runSwing(() -> {
+ list.setSelectedValue(ByteCopier.BYTE_STRING_TYPE, true);
+ JButton okButton = (JButton) getInstanceField("okButton", component);
+ okButton.doClick();
+ });
+
+ try {
+ waitForTasks();
+ }
+ catch (Exception e) {
+ Assert.fail();
+ }
+ }
+
+ private void copySpecial_ByteStringNoSpaces(ComponentProviderWrapper wrapper,
+ final DockingAction copyAction) {
+ wrapper.verifySelection();
+ executeOnSwingWithoutBlocking(() -> copyAction.actionPerformed(getActionContext(wrapper)));
+
+ // get the dialog and make a selection
+ JDialog dialog = waitForJDialog("Copy Special");
+ assertNotNull(dialog);
+ DockingDialog dockingDialog = (DockingDialog) dialog;
+ final DialogComponentProvider component =
+ (DialogComponentProvider) getInstanceField("component", dockingDialog);
+ Object listPanel = getInstanceField("listPanel", component);
+ final JList> list = (JList>) getInstanceField("list", listPanel);
+
+ runSwing(() -> {
+ list.setSelectedValue(ByteCopier.BYTE_STRING_NO_SPACE_TYPE, true);
+ JButton okButton = (JButton) getInstanceField("okButton", component);
+ okButton.doClick();
+ });
+
+ try {
+ waitForTasks();
+ }
+ catch (Exception e) {
+ Assert.fail("Unexpected exception waiting for tasks");
+ }
+ }
+
+ private void assertByteViewerBytes(String clipboardContents, Address address)
+ throws MemoryAccessException, AddressOutOfBoundsException {
+ Memory memory = program.getMemory();
+
+ //b090db777880db774893db77
+ // expecting a bytes string of the format: b0 90 db 77 78 80 db 77 48 93 db 77
+ String[] bytes = null;
+ if (clipboardContents.contains(" ")) {
+ bytes = clipboardContents.split("\\s");
+ }
+ else {
+ bytes = new String[clipboardContents.length() >> 1];
+ for (int i = 0; i < bytes.length; i++) {
+ int stringOffset = i * 2;
+ bytes[i] = clipboardContents.substring(stringOffset, stringOffset + 2);
+ }
+ }
+ for (int i = 0; i < bytes.length; i++) {
+ byte bite = memory.getByte(address.add(i));
+ if (!bytes[i].equals(Integer.toHexString(bite & 0xFF))) {
+ Msg.debug(this, "not equal at index: " + i);
+ }
+ assertEquals("Bytes not pasted as expected", bytes[i],
+ Integer.toHexString(bite & 0xFF));
+ }
+ }
+
+ private void clearSelectedBytes(Address address, long length) {
+ AddressSet addressSet = new AddressSet(address);
+ for (int i = 1; i < length; i++) {
+ addressSet.add(address.add(i));
+ }
+
+ int startTransaction = program.startTransaction("Test - Clear Bytes");
+ try {
+ ClearCmd clearCmd = new ClearCmd(addressSet);
+ clearCmd.applyTo(program);
+ }
+ finally {
+ program.endTransaction(startTransaction, true);
+ }
+ }
+
+ /*
+ * We remove the FieldPanel focus listeners for these tests, as when they lose focus,
+ * the selection mechanism does not work as expected. Focus changes can happen
+ * indeterminately during parallel batch testing.
+ */
+ private void removeFieldPanelFocusListeners(Container c) {
+ if (c instanceof FieldPanel) {
+ removeFocusListeners((JComponent) c);
+ }
+
+ Component[] children = c.getComponents();
+ for (Component child : children) {
+ if (!(child instanceof Container)) {
+ continue;
+ }
+ removeFieldPanelFocusListeners((Container) child);
+ }
+ }
+
+ private void removeFocusListeners(JComponent c) {
+ FocusListener[] listeners = c.getFocusListeners();
+ for (FocusListener l : listeners) {
+ c.removeFocusListener(l);
+ }
+ }
+
+//==================================================================================================
+// Inner Classes
+//==================================================================================================
+
+ interface ComponentProviderWrapper {
+ public Point getStartMouseDragLocation();
+
+ public ActionContext getContext();
+
+ public Point getEndMouseDragLocation();
+
+ public void clearSelection();
+
+ public void verifySelection();
+
+ public ComponentProvider getComponentProvider();
+ }
+
+ class ByteViewerWrapper implements ComponentProviderWrapper {
+ private final ByteViewerComponentProvider provider;
+
+ public ByteViewerWrapper(ComponentProvider provider) {
+ this.provider = (ByteViewerComponentProvider) provider;
+
+ removeFieldPanelFocusListeners(provider.getComponent());
+ }
+
+ @Override
+ public void verifySelection() {
+ Object byteViewerPanel = getInstanceField("panel", provider);
+ ByteBlockSelection byteBlockSelection =
+ (ByteBlockSelection) invokeInstanceMethod("getViewerSelection", byteViewerPanel);
+ assertTrue("No selection in the byte viewer.",
+ byteBlockSelection.getNumberOfRanges() > 0);
+ }
+
+ @Override
+ public Point getStartMouseDragLocation() {
+ return new Point(100, 30);
+ }
+
+ @Override
+ public Point getEndMouseDragLocation() {
+ return new Point(300, 30);
+ }
+
+ @Override
+ public ComponentProvider getComponentProvider() {
+ return provider;
+ }
+
+ @Override
+ public ActionContext getContext() {
+ return provider.getActionContext(null);
+ }
+
+ @Override
+ public void clearSelection() {
+ Object byteViewerPanel = getInstanceField("panel", provider);
+ runSwing(() -> {
+ invokeInstanceMethod("setViewerSelection", byteViewerPanel,
+ new Class[] { ByteBlockSelection.class },
+ new Object[] { new ByteBlockSelection() });
+ });
+
+ }
+ }
+
+ class CodeViewerWrapper implements ComponentProviderWrapper {
+
+ private final CodeViewerProvider provider;
+
+ public CodeViewerWrapper(CodeViewerProvider provider) {
+ this.provider = provider;
+
+ removeFieldPanelFocusListeners(provider.getComponent());
+ }
+
+ @Override
+ public void verifySelection() {
+ ProgramSelection selection = provider.getSelection();
+ if (!selection.isEmpty()) {
+ return;
+ }
+
+ CodeBrowserClipboardProvider clipboardProvider =
+ (CodeBrowserClipboardProvider) getInstanceField("codeViewerClipboardProvider",
+ provider);
+
+ assertTrue("No selection in the code browser.",
+ clipboardProvider.getStringContent() != null);
+ }
+
+ @Override
+ public Point getStartMouseDragLocation() {
+ return new Point(100, 110);
+ }
+
+ @Override
+ public Point getEndMouseDragLocation() {
+ return new Point(300, 210);
+ }
+
+ @Override
+ public ComponentProvider getComponentProvider() {
+ return provider;
+ }
+
+ @Override
+ public ActionContext getContext() {
+ return provider.getActionContext(null);
+ }
+
+ @Override
+ public void clearSelection() {
+ runSwing(() -> provider.programSelectionChanged(new ProgramSelection()));
+ }
+ }
+
+ class DecompilerWrapper implements ComponentProviderWrapper {
+
+ private final DecompilerProvider provider;
+
+ public DecompilerWrapper(ComponentProvider provider) {
+ this.provider = (DecompilerProvider) provider;
+
+ removeFieldPanelFocusListeners(provider.getComponent());
+ }
+
+ @Override
+ public void verifySelection() {
+ Object controller = getInstanceField("controller", provider);
+ Object decompilerPanel = getInstanceField("decompilerPanel", controller);
+ FieldPanel fieldPanel = (FieldPanel) getInstanceField("fieldPanel", decompilerPanel);
+ FieldSelection selection = fieldPanel.getSelection();
+ assertTrue("No selection in the decompile provider.", !selection.isEmpty());
+ }
+
+ @Override
+ public Point getStartMouseDragLocation() {
+ return new Point(10, 5);
+ }
+
+ @Override
+ public Point getEndMouseDragLocation() {
+ return new Point(210, 5);
+ }
+
+ @Override
+ public ComponentProvider getComponentProvider() {
+ return provider;
+ }
+
+ @Override
+ public ActionContext getContext() {
+ return provider.getActionContext(null);
+ }
+
+ @Override
+ public void clearSelection() {
+ runSwing(() -> provider.setSelection(new ProgramSelection()));
+ }
+ }
+
+ static class DummyTransferable implements Transferable {
+
+ @Override
+ public Object getTransferData(DataFlavor flavor)
+ throws UnsupportedFlavorException, IOException {
+ return null;
+ }
+
+ @Override
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[0];
+ }
+
+ @Override
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ return true;
+ }
+
+ }
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/processors/SetLanguageTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/processors/SetLanguageTest.java
new file mode 100644
index 0000000000..f3bafcc6e9
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/processors/SetLanguageTest.java
@@ -0,0 +1,307 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.processors;
+
+import static org.junit.Assert.*;
+
+import java.math.BigInteger;
+
+import org.junit.*;
+
+import docking.ActionContext;
+import docking.action.DockingActionIf;
+import docking.widgets.MultiLineLabel;
+import docking.widgets.OptionDialog;
+import docking.widgets.tree.GTree;
+import docking.widgets.tree.GTreeNode;
+import ghidra.framework.main.FrontEndTool;
+import ghidra.framework.main.datatree.DomainFileNode;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.plugin.importer.NewLanguagePanel;
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.model.address.*;
+import ghidra.program.model.lang.*;
+import ghidra.program.model.listing.*;
+import ghidra.program.model.symbol.RefType;
+import ghidra.program.model.symbol.SourceType;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+import ghidra.util.task.TaskMonitor;
+
+public class SetLanguageTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private TestEnv env;
+ private FrontEndTool frontEndTool;
+
+ private DockingActionIf setLanguageAction;
+ private GTreeNode notepadNode;
+ private DomainFile notepadFile;
+ private GTreeNode xyzFolderNode;
+
+ private AddressFactory addrFactory;
+
+ @Before
+ public void setUp() throws Exception {
+ env = new TestEnv();
+
+ setErrorGUIEnabled(true);
+ frontEndTool = env.getFrontEndTool();
+ env.showFrontEndTool();
+
+ setLanguageAction = getAction(frontEndTool, "LanguageProviderPlugin", "Set Language");
+
+ // NOTE: Only test translation from a supported language to another supported language
+
+// TODO: Change test data to a supported case (e.g., MIPS-32 to MIPS-64)
+
+ DomainFolder rootFolder = env.getProject().getProjectData().getRootFolder();
+
+ ProgramBuilder builder = new ProgramBuilder("notepad", "x86:LE:32:default");
+ Program p = builder.getProgram();
+
+ assertEquals(new LanguageID("x86:LE:32:default"), p.getLanguageID());
+ rootFolder.createFile("notepad", p, TaskMonitor.DUMMY);
+ env.release(p);
+ builder.dispose();
+
+ rootFolder.createFolder("XYZ");
+ GTree tree = findComponent(frontEndTool.getToolFrame(), GTree.class);
+ waitForTree(tree);
+
+ GTreeNode rootNode = tree.getRootNode();
+ xyzFolderNode = rootNode.getChild(0);
+ notepadNode = rootNode.getChild(1);
+ notepadFile = ((DomainFileNode) notepadNode).getDomainFile();
+
+ waitForSwing();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ env.dispose();
+ }
+
+ @Test
+ public void testActionEnablement() throws Exception {
+ assertTrue(setLanguageAction.isEnabled());
+ assertTrue(!setLanguageAction.isEnabledForContext(createContext(xyzFolderNode)));
+ assertTrue(setLanguageAction.isEnabledForContext(createContext(notepadNode)));
+ }
+
+ private Address addr(String address) {
+ return addrFactory.getAddress(address);
+ }
+
+ private void startSetLanguage(final LanguageID languageID, final CompilerSpecID compilerSpecID,
+ boolean isFailureCase) throws Exception {
+ if (languageID == null) {
+ throw new RuntimeException("languageID == null not allowed");
+ }
+ if (compilerSpecID == null) {
+ throw new RuntimeException("compilerSpecID == null not allowed");
+ }
+
+ // this triggers a modal dialog
+ runSwing(() -> {
+ ActionContext context = createContext(notepadNode);
+ assertTrue(setLanguageAction.isEnabledForContext(context));
+ setLanguageAction.actionPerformed(context);
+ }, false);
+
+ OptionDialog confirmDlg = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(confirmDlg);
+ MultiLineLabel msgLabel = findComponent(confirmDlg, MultiLineLabel.class);
+ assertNotNull(msgLabel);
+ assertTrue(msgLabel.getLabel().indexOf("Setting the language can not be undone") >= 0);
+ assertTrue(msgLabel.getLabel().indexOf("make a copy") > 0);
+
+ pressButtonByText(confirmDlg, "Ok");
+
+ final SetLanguageDialog dlg = waitForDialogComponent(SetLanguageDialog.class);
+ assertNotNull(dlg);
+ final NewLanguagePanel languagePanel =
+ (NewLanguagePanel) getInstanceField("selectLangPanel", dlg);
+ assertNotNull(languagePanel);
+
+ waitForSwing();
+
+ runSwing(() -> {
+ NewLanguagePanel selectLangPanel =
+ (NewLanguagePanel) getInstanceField("selectLangPanel", dlg);
+ selectLangPanel.setSelectedLcsPair(
+ new LanguageCompilerSpecPair(languageID, compilerSpecID));
+ }, true);
+
+ waitForSwing();
+
+ pressButtonByText(dlg, "OK");
+
+ if (!isFailureCase) {
+ confirmDlg = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(confirmDlg);
+ msgLabel = findComponent(confirmDlg, MultiLineLabel.class);
+ assertNotNull(msgLabel);
+ assertTrue(msgLabel.getLabel().indexOf("Would you like to Save") >= 0);
+
+ pressButtonByText(confirmDlg, "Save");
+ }
+ }
+
+ @Test
+ public void testReplaceLanguage() throws Exception {
+
+ startSetLanguage(new LanguageID("x86:LE:32:System Management Mode"),
+ new CompilerSpecID("default"), false);
+
+ waitForTasks();
+
+ Program p = (Program) notepadFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertNotNull(p);
+ try {
+ assertEquals(new LanguageID("x86:LE:32:System Management Mode"),
+ p.getLanguage().getLanguageID());
+
+ // TODO: Other checks needed ??
+
+ }
+ finally {
+ p.release(this);
+ }
+ }
+
+ @Test
+ public void testReplaceLanguageFailure() throws Exception {
+
+ startSetLanguage(new LanguageID("8051:BE:16:default"), new CompilerSpecID("default"), true);
+
+ final OptionDialog errDlg = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(errDlg);
+ MultiLineLabel msgLabel = findComponent(errDlg, MultiLineLabel.class);
+ assertNotNull(msgLabel);
+ assertTrue(msgLabel.getLabel().indexOf("Language translation not supported") >= 0);
+
+ pressButtonByText(errDlg, "OK");
+ closeAllWindows();
+ }
+
+ @Test
+ public void testReplaceLanguage2() throws Exception {
+
+ Program p = (Program) notepadFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ try {
+ int txId = p.startTransaction("set Language");
+ addrFactory = p.getAddressFactory();
+ ProgramContext pc = p.getProgramContext();
+ Register ax = pc.getRegister("ax");
+ Register ebp = pc.getRegister("ebp");
+ Register ebx = pc.getRegister("ebx");
+ pc.setValue(ax, addr("0x1001000"), addr("0x1001000"), BigInteger.valueOf(0x1234));
+ pc.setValue(ebp, addr("0x1001000"), addr("0x1001000"), BigInteger.valueOf(0x12345678));
+ pc.setValue(ebx, addr("0x1001000"), addr("0x1001000"), BigInteger.valueOf(0x12345678));
+ assertEquals(0x1234, pc.getValue(ax, addr("0x1001000"), false).longValue());
+ assertEquals(0x12345678, pc.getValue(ebp, addr("0x1001000"), false).longValue());
+ assertEquals(0x12345678, pc.getValue(ebx, addr("0x1001000"), false).longValue());
+ p.endTransaction(txId, true);
+
+ p.save(null, TaskMonitor.DUMMY);
+ }
+ finally {
+ p.release(this);
+ }
+
+ startSetLanguage(new LanguageID("x86:LE:32:default"), new CompilerSpecID("gcc"), false);
+ waitForTasks();
+
+ p = (Program) notepadFile.getDomainObject(this, true, false, TaskMonitor.DUMMY);
+ try {
+ addrFactory = p.getAddressFactory();
+ ProgramContext pc = p.getProgramContext();
+ Register ax = pc.getRegister("ax");
+ Register ebp = pc.getRegister("ebp");
+ Register ebx = pc.getRegister("ebx");
+ assertEquals(0x1234, pc.getValue(ax, addr("0x1001000"), false).longValue());
+ assertEquals(0x12345678, pc.getValue(ebp, addr("0x1001000"), false).longValue());
+ assertEquals(0x12345678, pc.getValue(ebx, addr("0x1001000"), false).longValue());
+ }
+ finally {
+ p.release(this);
+ }
+ }
+
+ @Test
+ public void testReplaceLanguage3() throws Exception {
+
+ Program p = (Program) notepadFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ addrFactory = p.getAddressFactory();
+ ProgramContext pc = p.getProgramContext();
+ Register eax = pc.getRegister("eax");
+ Register esi = pc.getRegister("esi");
+ Register edi = pc.getRegister("edi");
+ try {
+ int txId = p.startTransaction("set Language");
+
+ Function f = p.getListing().createFunction("BOB", addr("0x10041a8"),
+ new AddressSet(addr("0x10041a8"), addr("0x10041c0")), SourceType.USER_DEFINED);
+ f.setCustomVariableStorage(true);
+ ParameterImpl param = new ParameterImpl("PARAM_ONE", null, eax, p);
+ f.addParameter(param, SourceType.USER_DEFINED);
+ LocalVariableImpl local1 = new LocalVariableImpl("LOCAL_ONE", 0, null, esi, p);
+ LocalVariableImpl local2 = new LocalVariableImpl("LOCAL_TWO", 0, null, edi, p);
+
+ f.addLocalVariable(local1, SourceType.USER_DEFINED);
+ f.addLocalVariable(local2, SourceType.USER_DEFINED);
+
+ p.getReferenceManager().addRegisterReference(addr("0x10041b2"), 0, esi, RefType.DATA,
+ SourceType.USER_DEFINED);
+ p.getReferenceManager().addRegisterReference(addr("0x10041b3"), 0, edi, RefType.DATA,
+ SourceType.USER_DEFINED);
+
+ p.endTransaction(txId, true);
+
+ p.save(null, TaskMonitor.DUMMY);
+ }
+ finally {
+ p.release(this);
+ }
+
+ startSetLanguage(new LanguageID("x86:LE:32:default"), new CompilerSpecID("gcc"), false);
+ waitForTasks();
+
+ p = (Program) notepadFile.getDomainObject(this, true, false, TaskMonitor.DUMMY);
+ try {
+ addrFactory = p.getAddressFactory();
+
+ Function fun = p.getListing().getFunctionAt(addr("0x10041a8"));
+ Parameter[] params = fun.getParameters();
+ assertEquals(1, params.length);
+ assertEquals("PARAM_ONE", params[0].getName());
+ assertTrue(params[0].isRegisterVariable());
+ assertEquals(eax, params[0].getRegister());
+
+ Variable[] locals = fun.getLocalVariables();
+ assertEquals(2, locals.length);
+ assertEquals("LOCAL_ONE", locals[0].getName());
+ assertEquals("LOCAL_TWO", locals[1].getName());
+ assertTrue(params[0].isRegisterVariable());
+ assertEquals(esi, locals[0].getRegister());
+ assertEquals(edi, locals[1].getRegister());
+ }
+ finally {
+ p.release(this);
+ }
+ }
+}