From 4fd80959b77f1d3fa439525bae982bf1864b5d57 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:15:55 -0400 Subject: [PATCH 1/2] GP-6756 - Fixed 'Save As' dialog not allowing users to select a folder --- .../core/progmgr/ProgramSaveManager.java | 8 +- .../main/AbstractDataTreeDialog.java | 179 +++++++++++------- .../framework/main/DataTreeDialogTest.java | 58 +++++- 3 files changed, 167 insertions(+), 78 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java index 5a6992a38d..70c2925333 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java @@ -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) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java index 389f1bc862..bd9a5dc192 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java @@ -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(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java index 9539770432..20a3552b63 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java @@ -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(() -> { From 0a6606a2ac872e0976a3b9174b87fb249d589804 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:23:08 -0400 Subject: [PATCH 2/2] GP-6749 - Data Type Manager - Fixed the filter and added an 'Other' category --- .../core/datamgr/tree/DtFilterDialog.java | 7 +++++ .../core/datamgr/tree/DtFilterState.java | 26 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterDialog.java index f4af227db3..37d2cbf428 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterDialog.java @@ -35,6 +35,7 @@ public class DtFilterDialog extends DialogComponentProvider { private TypeComponent arraysComponent = new TypeComponent("Arrays"); private TypeComponent enumsComponent = new TypeComponent("Enums"); private TypeComponent functionsComponent = new TypeComponent("Functions"); + private TypeComponent otherComponent = new TypeComponent("Other"); private TypeComponent pointersComponent = new TypeComponent("Pointers"); private TypeComponent structuresComponent = new TypeComponent("Structures"); private TypeComponent unionsComponent = new TypeComponent("Unions"); @@ -77,6 +78,7 @@ public class DtFilterDialog extends DialogComponentProvider { newState.setArraysFilter(arraysComponent.getFilter()); newState.setEnumsFilter(enumsComponent.getFilter()); newState.setFunctionsFilter(functionsComponent.getFilter()); + newState.setOtherFilter(otherComponent.getFilter()); newState.setPointersFilter(pointersComponent.getFilter()); newState.setStructuresFilter(structuresComponent.getFilter()); newState.setUnionsFilter(unionsComponent.getFilter()); @@ -108,6 +110,8 @@ public class DtFilterDialog extends DialogComponentProvider { panel.add(structuresComponent.getRight()); panel.add(unionsComponent.getLeft()); panel.add(unionsComponent.getRight()); + panel.add(otherComponent.getLeft()); + panel.add(otherComponent.getRight()); GButton selectAll = new GButton("Select All"); GButton deselectAll = new GButton("Deselect All"); @@ -145,6 +149,8 @@ public class DtFilterDialog extends DialogComponentProvider { return List.of( arraysComponent.typeCb, arraysComponent.typeDefCb, + otherComponent.typeCb, + otherComponent.typeDefCb, enumsComponent.typeCb, enumsComponent.typeDefCb, functionsComponent.typeCb, @@ -161,6 +167,7 @@ public class DtFilterDialog extends DialogComponentProvider { private void initCheckBoxes() { arraysComponent.init(filterState.getArraysFilter()); + otherComponent.init(filterState.getOtherFilter()); enumsComponent.init(filterState.getEnumsFilter()); functionsComponent.init(filterState.getFunctionsFilter()); pointersComponent.init(filterState.getPointersFilter()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterState.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterState.java index 825711aa2f..9d627c9273 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterState.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DtFilterState.java @@ -21,7 +21,6 @@ import ghidra.app.plugin.core.datamgr.util.DataTypeUtils; import ghidra.framework.options.SaveState; import ghidra.program.model.data.*; import ghidra.program.model.data.Enum; -import ghidra.program.model.listing.Function; /** * A simple object to store various filter settings for the data type provider. @@ -34,6 +33,7 @@ public class DtFilterState { private DtTypeFilter enumsFilter = new DtTypeFilter("Enums"); private DtTypeFilter functionsFilter = new DtTypeFilter("Functions"); private DtTypeFilter structuresFilter = new DtTypeFilter("Structures"); + private DtTypeFilter otherFilter = new DtTypeFilter("Other"); private DtTypeFilter pointersFilter = new DtTypeFilter("Pointers"); private DtTypeFilter unionsFilter = new DtTypeFilter("Unions"); @@ -46,6 +46,7 @@ public class DtFilterState { public DtFilterState copy() { DtFilterState filterState = new DtFilterState(); filterState.arraysFilter = arraysFilter.copy(); + filterState.otherFilter = otherFilter.copy(); filterState.enumsFilter = enumsFilter.copy(); filterState.functionsFilter = functionsFilter.copy(); filterState.structuresFilter = structuresFilter.copy(); @@ -66,6 +67,18 @@ public class DtFilterState { this.arraysFilter = filter; } + public boolean isShowOther() { + return otherFilter.isTypeActive(); + } + + public DtTypeFilter getOtherFilter() { + return otherFilter; + } + + public void setOtherFilter(DtTypeFilter filter) { + this.otherFilter = filter; + } + public boolean isShowEnums() { return enumsFilter.isTypeActive(); } @@ -155,7 +168,7 @@ public class DtFilterState { return passes(enumsFilter, dt); } - if (baseDt instanceof Function) { + if (baseDt instanceof FunctionDefinition) { return passes(functionsFilter, dt); } @@ -167,7 +180,7 @@ public class DtFilterState { return passes(unionsFilter, dt); } - return true; + return passes(otherFilter, dt); } private boolean passes(DtTypeFilter filter, DataType dt) { @@ -184,6 +197,7 @@ public class DtFilterState { ss.putSaveState(arraysFilter.getName(), arraysFilter.save()); ss.putSaveState(enumsFilter.getName(), enumsFilter.save()); ss.putSaveState(functionsFilter.getName(), functionsFilter.save()); + ss.putSaveState(otherFilter.getName(), otherFilter.save()); ss.putSaveState(pointersFilter.getName(), pointersFilter.save()); ss.putSaveState(structuresFilter.getName(), structuresFilter.save()); ss.putSaveState(unionsFilter.getName(), unionsFilter.save()); @@ -201,6 +215,7 @@ public class DtFilterState { arraysFilter = DtTypeFilter.restore("Arrays", ss.getSaveState("Arrays")); enumsFilter = DtTypeFilter.restore("Enums", ss.getSaveState("Enums")); functionsFilter = DtTypeFilter.restore("Functions", ss.getSaveState("Functions")); + otherFilter = DtTypeFilter.restore("Other", ss.getSaveState("Other")); pointersFilter = DtTypeFilter.restore("Pointers", ss.getSaveState("Pointers")); structuresFilter = DtTypeFilter.restore("Structures", ss.getSaveState("Structures")); unionsFilter = DtTypeFilter.restore("Unions", ss.getSaveState("Unions")); @@ -208,8 +223,8 @@ public class DtFilterState { @Override public int hashCode() { - return Objects.hash(arraysFilter, enumsFilter, functionsFilter, pointersFilter, - structuresFilter, unionsFilter); + return Objects.hash(arraysFilter, enumsFilter, functionsFilter, + otherFilter, pointersFilter, structuresFilter, unionsFilter); } @Override @@ -227,6 +242,7 @@ public class DtFilterState { return Objects.equals(arraysFilter, other.arraysFilter) && Objects.equals(enumsFilter, other.enumsFilter) && Objects.equals(functionsFilter, other.functionsFilter) && + Objects.equals(otherFilter, other.otherFilter) && Objects.equals(pointersFilter, other.pointersFilter) && Objects.equals(structuresFilter, other.structuresFilter) && Objects.equals(unionsFilter, other.unionsFilter);