Merge remote-tracking branch 'origin/GP-6756-dragonmacher-save-as-folder-issue' into Ghidra_12.1

This commit is contained in:
Ryan Kurtz
2026-04-30 11:42:50 -04:00
3 changed files with 167 additions and 78 deletions
@@ -214,10 +214,14 @@ class ProgramSaveManager {
}
try {
DataTreeDialog dialog = getSaveDialog();
String filename = program.getDomainFile().getName();
DomainFile domainFile = program.getDomainFile();
String filename = domainFile.getName();
dialog.setTitle("Save As (" + filename + ")");
dialog.setNameText(filename + ".1");
dialog.setSelectedFolder(program.getDomainFile().getParent());
DomainFolder parent = domainFile.getParent();
dialog.setSelectedFolder(parent);
treeDialogCancelled = true;
tool.showDialog(dialog);
if (!treeDialogCancelled) {
@@ -193,6 +193,10 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
return nameField.getText();
}
public String getFolderText() {
return folderNameLabel.getText();
}
public void setNameText(String name) {
// We need to run this code in a task since the tree may already be processing other tasks
// that would override this setting when they are run. But putting this task in the queue,
@@ -246,89 +250,124 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider
return domainFolder;
}
/**
* TreeSelectionListener method that is called whenever the value of the selection changes.
* @param e the event that characterizes the change.
*/
@Override
public void valueChanged(GTreeSelectionEvent e) {
clearStatusText();
if (type == CHOOSE_FOLDER) {
domainFolder = treePanel.getSelectedDomainFolder();
if (domainFolder != null) {
DomainFolder folderParent = domainFolder.getParent();
if (folderParent != null) {
folderNameLabel.setText(folderParent.getPathname());
}
else {
folderNameLabel.setText(" ");
}
nameField.setText(domainFolder.getName());
}
else {
domainFile = treePanel.getSelectedDomainFile();
if (domainFile != null) {
domainFolder = domainFile.getParent();
DomainFolder grandParent = domainFolder.getParent();
if (grandParent != null) {
folderNameLabel.setText(grandParent.getPathname());
}
else {
folderNameLabel.setText("");
}
nameField.setText(domainFolder.getName());
}
else {
domainFolder = project.getProjectData().getRootFolder();
folderNameLabel.setText(domainFolder.getPathname());
nameField.setText(domainFolder.getName());
}
}
updateFromTreeSelectionInFolderMode();
}
else {
domainFile = treePanel.getSelectedDomainFile();
if (domainFile != null) {
LinkFileInfo linkInfo = domainFile.getLinkInfo();
if (linkInfo != null && linkInfo.isFolderLink()) {
// Ensure we don't have a folder name conflict
if (domainFile.getParent().getFolder(domainFile.getName()) == null) {
domainFolder = linkInfo.getLinkedFolder();
domainFile = null;
}
}
else {
folderNameLabel.setText(domainFile.getParent().getPathname());
nameField.setText(domainFile.getName());
domainFolder = domainFile.getParent();
}
}
if (domainFile == null) {
if (domainFolder == null) {
domainFolder = treePanel.getSelectedDomainFolder();
if (domainFolder == null) {
domainFolder = project.getProjectData().getRootFolder();
}
}
folderNameLabel.setText(domainFolder.getPathname());
if (nameField.isEditable()) {
if (nameField.getText().length() > 0) {
nameField.selectAll();
}
}
else {
nameField.setText("");
}
}
updateFromTreeSelectionInFileMode();
}
String text = nameField.getText();
setOkEnabled((text != null) && !text.isEmpty());
}
private void updateFromTreeSelectionInFileMode() {
DomainFile newFile = treePanel.getSelectedDomainFile();
if (isFolderLink(newFile)) {
updateFromFolderLink(newFile);
return;
}
// not a folder link; see if we have a new file selected
if (newFile != null) {
domainFile = newFile;
domainFolder = domainFile.getParent();
String pathname = domainFolder.getPathname();
folderNameLabel.setText(pathname);
String filename = domainFile.getName();
nameField.setText(filename);
return;
}
// No selected domain file
domainFile = null;
domainFolder = getSelectedFolder();
String pathname = domainFolder.getPathname();
folderNameLabel.setText(pathname);
updateNameFieldTextForNoFileSelected();
}
private void updateNameFieldTextForNoFileSelected() {
if (!nameField.isEditable()) {
nameField.setText("");
return;
}
if (nameField.getText().length() > 0) {
nameField.selectAll();
}
}
private void updateFromFolderLink(DomainFile newFile) {
LinkFileInfo linkInfo = newFile.getLinkInfo();
DomainFolder folder = newFile.getParent();
String filename = newFile.getName();
// Ensure we don't have a folder name conflict
if (folder.getFolder(filename) == null) {
domainFolder = linkInfo.getLinkedFolder();
if (domainFolder == null) {
domainFolder = getSelectedFolder();
}
domainFile = null;
folderNameLabel.setText(domainFolder.getPathname());
updateNameFieldTextForNoFileSelected();
return;
}
domainFile = newFile;
folderNameLabel.setText(folder.getPathname());
nameField.setText(filename);
domainFolder = folder;
}
private boolean isFolderLink(DomainFile file) {
if (file == null) {
return false;
}
LinkFileInfo linkInfo = file.getLinkInfo();
return linkInfo != null && linkInfo.isFolderLink();
}
private void updateFromTreeSelectionInFolderMode() {
// The tree selection has changed and we are in FOLDER mode. Update the folder selection
// based on type of node selected.
domainFolder = getSelectedFolder();
DomainFolder folderParent = domainFolder.getParent();
if (folderParent == null) {
folderParent = domainFolder; // root folder; no parent
}
folderNameLabel.setText(folderParent.getPathname());
nameField.setText(domainFolder.getName());
}
/**
* Returns the selected folder, or the selected file's parent or the root folder.
* @return the folder
*/
private DomainFolder getSelectedFolder() {
DomainFolder folder = treePanel.getSelectedDomainFolder();
if (folder != null) {
return folder;
}
DomainFile file = treePanel.getSelectedDomainFile();
if (file != null) {
return file.getParent();
}
return project.getProjectData().getRootFolder();
}
@Override
public void actionPerformed(ActionEvent event) {
int index = projectComboBox.getSelectedIndex();
@@ -41,6 +41,8 @@ import ghidra.util.task.TaskMonitor;
public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
private static final String TEST_FOLDER_NAME = "TestFolder";
private TestEnv env;
private FrontEndTool frontEndTool;
private DataTreeDialog dialog;
@@ -70,6 +72,10 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
DomainFolder domainFolder = ProjectDataUtils.createDomainFolderPath(rootFolder, path);
result.add(domainFolder.createFile(filename, p, TaskMonitor.DUMMY));
}
// add a test folder
ProjectDataUtils.createDomainFolderPath(rootFolder, "/" + TEST_FOLDER_NAME);
builder.dispose();
waitForSwing();
return result;
@@ -116,7 +122,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNameHasText(true);
// select a folder--text remains; button enabled
selectFolder();
selectRootFolder();
assertOK(true);
assertNameHasText(true);
@@ -125,6 +131,16 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertOK(true);
assertNameHasText(true);
// select a non-root folder--text remains; button enabled
selectTestFolder();
assertOK(true);
assertNameHasText(true);
assertFolderText("/" + TEST_FOLDER_NAME);
deselectFolder();
assertOK(true);
assertNameHasText(true);
// clear text--disabled
clearText();
assertOK(false);
@@ -147,7 +163,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNameHasText(true);
// select a folder--text remains; button enabled
selectFolder();
selectRootFolder();
assertOK(true);
assertNameHasText(true);
@@ -185,7 +201,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNameHasText(true); // "/"
// select a folder--enabled
selectFolder();
selectRootFolder();
assertOK(true);
assertNameHasText(true);
@@ -212,7 +228,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNameHasText(false);
// select a folder--disabled
selectFolder();
selectRootFolder();
assertOK(false);
// de-select a folder--disabled
@@ -237,7 +253,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNameHasText(false);
// select a folder--disabled
selectFolder();
selectRootFolder();
assertOK(false);
// de-select a folder--disabled
@@ -321,7 +337,7 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
});
}
private void selectFolder() {
private void selectRootFolder() {
final AtomicBoolean result = new AtomicBoolean(false);
final GTree gTree = getGTree();
runSwing(() -> {
@@ -340,6 +356,31 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
waitForTree(gTree);
}
private void selectTestFolder() {
AtomicBoolean result = new AtomicBoolean(false);
GTree gTree = getGTree();
runSwing(() -> {
GTreeNode root = gTree.getViewRoot();
if (root == null) {
return;
}
GTreeNode folderNode = root.getChild(TEST_FOLDER_NAME);
if (folderNode != null) {
gTree.expandPath(root);
gTree.setSelectedNode(folderNode);
result.set(true);
}
});
if (!result.get()) {
Assert.fail("Unable to select root folder");
}
waitForTree(gTree);
}
private void deselectFile() {
clearSelection();
}
@@ -350,6 +391,11 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
waitForTree(gTree);
}
private void assertFolderText(String expectedName) {
String actualName = runSwing(() -> dialog.getFolderText());
assertEquals("Dialog folder path not correct", expectedName, actualName);
}
private void assertNameHasText(boolean hasText) {
final AtomicBoolean result = new AtomicBoolean();
runSwing(() -> {