From 8234bfb14a3a634cc15d0c8f699f4dff248c894d Mon Sep 17 00:00:00 2001 From: ghidravision Date: Thu, 6 Mar 2025 18:54:59 +0000 Subject: [PATCH 1/2] GP-5327 - Create Structure Dialog - New text category box & existing categories chooser --- .../DefaultDataTypeManagerService.java | 10 + .../core/data/CreateStructureAction.java | 25 +- .../core/data/CreateStructureDialog.java | 410 ++++++++++----- .../core/datamgr/DataTypeManagerPlugin.java | 17 + .../core/datamgr/archive/DataTypeIndexer.java | 55 ++- .../datamgr/util/DataTypeChooserDialog.java | 96 +++- .../app/services/DataTypeManagerService.java | 9 + .../app/services/DataTypeQueryService.java | 18 +- .../datatype/CategoryPathSelectionEditor.java | 466 ++++++++++++++++++ .../datatype/DataTypeSelectionDialog.java | 16 +- .../util/parser/FunctionSignatureParser.java | 5 + .../datatype/DataTypeSelectionDialogTest.java | 5 +- .../TestDoubleDataTypeManagerService.java | 12 +- 13 files changed, 974 insertions(+), 170 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DefaultDataTypeManagerService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DefaultDataTypeManagerService.java index e1c6a94776..e2cff4fa1f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DefaultDataTypeManagerService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DefaultDataTypeManagerService.java @@ -93,6 +93,11 @@ public class DefaultDataTypeManagerService extends DefaultDataTypeArchiveService throw new UnsupportedOperationException(); } + @Override + public CategoryPath getCategoryPath(TreePath selectedPath) { + throw new UnsupportedOperationException(); + } + @Override public List getFavorites() { throw new UnsupportedOperationException(); @@ -111,6 +116,11 @@ public class DefaultDataTypeManagerService extends DefaultDataTypeArchiveService return dataTypes; } + @Override + public List getSortedCategoryPathList() { + throw new UnsupportedOperationException(); + } + @Override public void removeDataTypeManagerChangeListener(DataTypeManagerChangeListener listener) { throw new UnsupportedOperationException(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureAction.java index ebb9da5250..ee0d5a33c5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureAction.java @@ -4,9 +4,9 @@ * 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. @@ -40,7 +40,6 @@ class CreateStructureAction extends ListingContextAction { new String[] { "Data", "Create Structure..." }; private DataPlugin plugin; - private CreateStructureDialog createStructureDialog; public CreateStructureAction(DataPlugin plugin) { super("Create Structure", plugin.getName()); @@ -49,25 +48,12 @@ class CreateStructureAction extends ListingContextAction { setKeyBindingData(new KeyBindingData(KeyEvent.VK_OPEN_BRACKET, InputEvent.SHIFT_DOWN_MASK)); this.plugin = plugin; - setEnabled(true); - createStructureDialog = new CreateStructureDialog(plugin.getTool()); } - @Override - public void dispose() { - super.dispose(); - - createStructureDialog.dispose(); - } - - /** - * Method called when the action is invoked. - */ @Override public void actionPerformed(ListingActionContext programActionContext) { Program program = programActionContext.getProgram(); ProgramSelection sel = programActionContext.getSelection(); - if (sel != null && !sel.isEmpty()) { InteriorSelection interiorSel = sel.getInteriorSelection(); if (interiorSel != null) { @@ -113,8 +99,8 @@ class CreateStructureAction extends ListingContextAction { return; } - Structure userChoice = - createStructureDialog.showCreateStructureDialog(program, tempStructure); + CreateStructureDialog dialog = new CreateStructureDialog(plugin.getTool()); + Structure userChoice = dialog.showCreateStructureDialog(program, tempStructure); if (userChoice != null) { CreateStructureInStructureCmd cmd = new CreateStructureInStructureCmd(userChoice, @@ -162,8 +148,9 @@ class CreateStructureAction extends ListingContextAction { return; } + CreateStructureDialog dialog = new CreateStructureDialog(plugin.getTool()); Structure userChoice = - createStructureDialog.showCreateStructureDialog(program, tempStructure); + dialog.showCreateStructureDialog(program, tempStructure); // exit if the user cancels the operation if (userChoice != null) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureDialog.java index cfb7bca25e..4aa7939d35 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/data/CreateStructureDialog.java @@ -15,10 +15,10 @@ */ package ghidra.app.plugin.core.data; -import java.awt.Component; -import java.awt.Dimension; +import java.awt.*; import java.awt.event.*; import java.util.*; +import java.util.List; import javax.swing.*; import javax.swing.border.TitledBorder; @@ -28,25 +28,26 @@ import javax.swing.table.*; import javax.swing.text.BadLocationException; import javax.swing.text.Document; +import org.apache.commons.lang3.StringUtils; + import docking.ReusableDialogComponentProvider; import docking.widgets.button.GRadioButton; import docking.widgets.table.*; -import generic.theme.GThemeDefaults.Colors; import ghidra.app.services.DataTypeManagerService; import ghidra.app.util.ToolTipUtils; +import ghidra.app.util.datatype.CategoryPathSelectionEditor; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.data.*; import ghidra.program.model.listing.Program; import ghidra.util.*; import ghidra.util.exception.DuplicateNameException; +import ghidra.util.layout.PairLayout; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; /** * A dialog that allows the user to create a new structure based upon providing * a new name or by using the name of an existing structure. - * - * */ public class CreateStructureDialog extends ReusableDialogComponentProvider { private static final String NEW_STRUCTURE_STATUS_PREFIX = "Creating new structure: "; @@ -56,14 +57,15 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { private static final String PATH_COLUMN_NAME = "Path"; private JTextField nameTextField; + private CategoryPathSelectionEditor categoryPathEditor; private GhidraTable matchingStructuresTable; private StructureTableModel structureTableModel; private Structure currentStructure; private Program currentProgram; private PluginTool pluginTool; - private TitledBorder nameBorder; - private TitledBorder structureBorder; + private JRadioButton createNewStructButton; + private JRadioButton useExistingStructButton; private JRadioButton exactMatchButton; private JRadioButton sizeMatchButton; private GhidraTableFilterPanel filterPanel; @@ -97,23 +99,65 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { private JPanel buildMainPanel() { JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - - mainPanel.add(buildNameTextFieldPanel()); - mainPanel.add(Box.createVerticalStrut(10)); - - mainPanel.add(buildMatchingStructurePanel()); - + mainPanel.add(createChoicePanel()); setStatusJustification(SwingConstants.LEFT); - setCreateStructureByName(true); - mainPanel.getAccessibleContext().setAccessibleName("Create Structure"); return mainPanel; } - private JPanel buildNameTextFieldPanel() { - JPanel namePanel = new JPanel(); - namePanel.setLayout(new BoxLayout(namePanel, BoxLayout.Y_AXIS)); - nameBorder = BorderFactory.createTitledBorder("Create Structure By Name"); - namePanel.setBorder(nameBorder); + private JPanel createChoicePanel() { + JPanel radioChoicePanel = new JPanel(new BorderLayout()); + + createNewStructButton = new GRadioButton("Create New"); + createNewStructButton.getAccessibleContext().setAccessibleName("Create New"); + useExistingStructButton = new GRadioButton("Use Existing"); + useExistingStructButton.getAccessibleContext().setAccessibleName("Use Existing"); + + ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(createNewStructButton); + buttonGroup.add(useExistingStructButton); + createNewStructButton.setSelected(true); + ItemListener choiceListener = event -> updateEnablement(); + createNewStructButton.addItemListener(choiceListener); + useExistingStructButton.addItemListener(choiceListener); + + JPanel createNewStructPanel = new JPanel(); + createNewStructPanel.setLayout(new BoxLayout(createNewStructPanel, BoxLayout.Y_AXIS)); + // force the radio button to the left for clarity + createNewStructPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + // indent everything under the radio button + createNewStructPanel.setBorder(BorderFactory.createEmptyBorder(5, 30, 15, 5)); + + JPanel useExistingStructPanel = new JPanel(); + useExistingStructPanel.setLayout(new BoxLayout(useExistingStructPanel, BoxLayout.Y_AXIS)); + useExistingStructPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + useExistingStructPanel.setBorder(BorderFactory.createEmptyBorder(0, 30, 10, 5)); + + createNewStructPanel.add(buildCreateNewStructPanel()); + useExistingStructPanel.add(buildMatchingStructPanel()); + + JPanel top = new JPanel(); + top.setLayout(new BoxLayout(top, BoxLayout.PAGE_AXIS)); + top.add(createNewStructButton); + top.add(createNewStructPanel); + + JPanel center = new JPanel(); + center.setLayout(new BoxLayout(center, BoxLayout.PAGE_AXIS)); + center.add(useExistingStructButton); + center.add(useExistingStructPanel); + + // we would like the structure table to get all extra space, so put it in the center + radioChoicePanel.add(top, BorderLayout.NORTH); + radioChoicePanel.add(center, BorderLayout.CENTER); + + return radioChoicePanel; + } + + private JPanel buildCreateNewStructPanel() { + JPanel newStructPanel = new JPanel(); + newStructPanel.setLayout(new PairLayout()); + newStructPanel.setToolTipText("Enter a name and category (optional)"); + + JLabel nameLabel = new JLabel("Name: "); nameTextField = new JTextField() { // make sure our height doesn't stretch @@ -124,16 +168,15 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return d; } }; + // Allow user to click on the text field to re-activate "create new" panel without forcing + // a click on the radio button nameTextField.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { - setCreateStructureByName(true); - nameTextField.requestFocus(); + createNewStructButton.setSelected(true); + updateEnablement(); } }); - nameTextField.getAccessibleContext().setAccessibleName("Name Text"); - namePanel.add(nameTextField); - nameTextField.getDocument().addDocumentListener(new DocumentListener() { @Override public void changedUpdate(DocumentEvent event) { @@ -153,7 +196,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { private void checkText(Document document) { try { String text = document.getText(0, document.getLength()); - if ((text == null) || (text.trim().length() == 0)) { + if (StringUtils.isBlank(text)) { okButton.setEnabled(false); updateStatusText(true, null); } @@ -167,17 +210,82 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { } } }); - namePanel.getAccessibleContext().setAccessibleName("Name Text"); - return namePanel; + + JLabel categoryLabel = new JLabel("Category: "); + buildCategoryPathEditor(); + + newStructPanel.add(nameLabel); + newStructPanel.add(nameTextField); + newStructPanel.add(categoryLabel); + newStructPanel.add(categoryPathEditor.getEditorComponent()); + + return newStructPanel; } - private JPanel buildMatchingStructurePanel() { + private void buildCategoryPathEditor() { + categoryPathEditor = new CategoryPathSelectionEditor(pluginTool); + categoryPathEditor.getEditorComponent() + .getAccessibleContext() + .setAccessibleName("Category"); + // make sure the "Category: " text field size matches the "Name: " text field size + categoryPathEditor.getEditorComponent().setMaximumSize(nameTextField.getMaximumSize()); + categoryPathEditor.addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent event) { + updateStatus(event.getDocument()); + } + + @Override + public void insertUpdate(DocumentEvent event) { + updateStatus(event.getDocument()); + } + + @Override + public void removeUpdate(DocumentEvent event) { + updateStatus(event.getDocument()); + } + + private void updateStatus(Document document) { + try { + String text = document.getText(0, document.getLength()); + if (StringUtils.isBlank(text)) { + updateStatusText(true, null); + } + else { + updateStatusText(true, "Using category: " + text); + } + } + catch (BadLocationException ble) { + // nothing we can do here + } + } + }); + // Allow the user to re-activate the "new struct" panel without forcing toggle click. Use + // FocusListener because @CategoryPathSelectionEditor.java already contains a mouse listener + // and would override this one. + categoryPathEditor.addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + createNewStructButton.setSelected(true); + updateEnablement(); + } + }); + } + + private JPanel buildMatchingStructPanel() { JPanel structurePanel = new JPanel(); structurePanel.setLayout(new BoxLayout(structurePanel, BoxLayout.Y_AXIS)); - structureBorder = BorderFactory.createTitledBorder("Use Existing Structure"); - structurePanel.setBorder(structureBorder); GTable table = buildMatchingStructuresTable(); + // allow user to re-activate the "use existing" panel without forcing a radio button click. + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + useExistingStructButton.setSelected(true); + updateEnablement(); + } + }); + filterPanel = new GhidraTableFilterPanel<>(table, structureTableModel) { // make sure our height doesn't stretch @Override @@ -190,14 +298,13 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { JScrollPane scrollPane = new JScrollPane(table); scrollPane.getAccessibleContext().setAccessibleName("Scroll"); + structurePanel.add(scrollPane); structurePanel.add(Box.createVerticalStrut(10)); - filterPanel.getAccessibleContext().setAccessibleName("Structure Filter"); structurePanel.add(filterPanel); structurePanel.add(Box.createVerticalStrut(10)); structurePanel.add(buildMatchingStyelPanel()); structurePanel.add(Box.createVerticalStrut(10)); - structurePanel.getAccessibleContext().setAccessibleName("Matching Structure"); return structurePanel; } @@ -216,32 +323,41 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { TableColumn column = columnModel.getColumn(i); column.setCellRenderer(cellRenderer); } - matchingStructuresTable.getColumnModel().getColumn(0); ListSelectionModel lsm = matchingStructuresTable.getSelectionModel(); lsm.addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - ListSelectionModel sourceListSelectionModel = (ListSelectionModel) e.getSource(); - if ((sourceListSelectionModel != null) && - !(sourceListSelectionModel.isSelectionEmpty())) { - // show the user that the structure choice is now - // coming from the list of current structures - Structure structure = ((StructureWrapper) matchingStructuresTable - .getValueAt(matchingStructuresTable.getSelectedRow(), 0)) - .getStructure(); - updateStatusText(false, structure.getName()); - setCreateStructureByName(false); - } - else { - updateStatusText(true, nameTextField.getText()); - setCreateStructureByName(true); - } + if (e.getValueIsAdjusting()) { + return; } + + ListSelectionModel selectionModel = (ListSelectionModel) e.getSource(); + if (selectionModel != null && !selectionModel.isSelectionEmpty()) { + // Show the user that the structure choice is now coming from the table + useExistingStructButton.setSelected(true); + } + + updateEnablement(); }); + matchingStructuresTable.getAccessibleContext().setAccessibleName("Matching Structures"); return matchingStructuresTable; } + private void updateStatus() { + + clearStatusText(); + + if (useExistingStructButton.isSelected()) { + Structure structure = getSelectedStructure(); + if (structure != null) { + updateStatusText(false, structure.getName()); + } + } + else { + updateStatusText(true, nameTextField.getText()); + } + } + private JPanel buildMatchingStyelPanel() { JPanel matchingStylePanel = new JPanel() { @Override @@ -275,51 +391,49 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { matchingStylePanel.add(exactMatchButton); matchingStylePanel.add(sizeMatchButton); - matchingStylePanel.getAccessibleContext().setAccessibleName("Matching Style"); return matchingStylePanel; } - // toggles whether the structure being created is new, based upon the name field, or a current - // structure, based upon a structure in the table. This method updates the GUI to reflect the - // current creation state. - private void setCreateStructureByName(boolean createStructureByName) { - if (createStructureByName) { - nameBorder.setTitleColor(Colors.FOREGROUND); - structureBorder.setTitleColor(Colors.FOREGROUND_DISABLED); - } - else { - nameBorder.setTitleColor(Colors.FOREGROUND_DISABLED); - structureBorder.setTitleColor(Colors.FOREGROUND); - } - - nameTextField.setEnabled(createStructureByName); - - if (createStructureByName) { + // Toggles whether the structure being created is new, based upon the name field, or existing, + // based upon a structure in the table. + private void updateEnablement() { + if (createNewStructButton.isSelected()) { + nameTextField.setEnabled(true); + categoryPathEditor.setEnabled(true); + matchingStructuresTable.setEnabled(false); + exactMatchButton.setEnabled(false); + sizeMatchButton.setEnabled(false); matchingStructuresTable.clearSelection(); } - + else { + nameTextField.setEnabled(false); + categoryPathEditor.setEnabled(false); + matchingStructuresTable.setEnabled(true); + exactMatchButton.setEnabled(true); + sizeMatchButton.setEnabled(true); + } rootPanel.repaint(); + + updateStatus(); } - // populates the table with structures that match the one the passed to - // this class in terms of data contained + // Populates the table with structures that match the one the passed to this class in terms of + // data contained private void searchForMatchingStructures(final Program program, final Structure structure) { - Swing.runLater(() -> { - // Get the structures from the DataTypeManagers of the - // DataTypeManagerService - DataTypeManagerService service = pluginTool.getService(DataTypeManagerService.class); - DataTypeManager[] dataTypeManagers = null; + // Get the structures from the DataTypeManagers of the DataTypeManagerService + DataTypeManagerService service = pluginTool.getService(DataTypeManagerService.class); - if (service != null) { - dataTypeManagers = service.getDataTypeManagers(); - } - else { - dataTypeManagers = new DataTypeManager[] { program.getDataTypeManager() }; - } + DataTypeManager[] dataTypeManagers = null; - getMatchingStructuresFromDataTypeManagers(structure, dataTypeManagers); - }); + if (service != null) { + dataTypeManagers = service.getDataTypeManagers(); + } + else { + dataTypeManagers = new DataTypeManager[] { program.getDataTypeManager() }; + } + + getMatchingStructuresFromDataTypeManagers(structure, dataTypeManagers); } private void getMatchingStructuresFromDataTypeManagers(Structure structure, @@ -330,8 +444,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { Iterator structureIterator = dataTypeManager.getAllStructures(); while (structureIterator.hasNext()) { - // only add structures that match the one that was - // passed to this dialog + // only add structures that match the one that was passed to this dialog Structure nextStructure = structureIterator.next(); if (compareStructures(nextStructure, structure)) { @@ -343,8 +456,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { structureTableModel.setData(dataList); } - // compares structures depending upon the type of matching that is being - // used + // compares structures depending upon the type of matching that is being used private boolean compareStructures(Structure structureA, Structure structureB) { if (sizeMatchButton.isSelected()) { return compareStructuresBySize(structureA, structureB); @@ -358,9 +470,9 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return (structureA.getLength() == structureB.getLength()); } - // Compares the two structures based upon the data contained. This method - // is used instead of isEquivalent() to avoid the comparison of data field - // names, which is not a concern for this class. + // Compares the two structures based upon the data contained. This method is used instead of + // isEquivalent() to avoid the comparison of data field names, which is not a concern for this + // class. private boolean compareStructuresByData(Structure structureA, Structure structureB) { if (structureA.getLength() != structureB.getLength()) { @@ -382,12 +494,11 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return false; } - // called by compareStructures() to compare the data that the structures - // contain + // called by compareStructures() to compare the data that the structures contain private boolean compareDataTypeComponents(DataTypeComponent dtcA, DataTypeComponent dtcB) { - // be sure to do the easiest comparisons first, those based on - // equality and then do the possibly recursive calls last + // be sure to do the easiest comparisons first, those based on equality and then do the + // possibly recursive calls last if ((dtcA.getLength() == dtcB.getLength()) && (dtcA.getOffset() == dtcB.getOffset()) && (dtcA.getOrdinal() == dtcB.getOrdinal()) && compareDataTypes(dtcA.getDataType(), dtcB.getDataType())) { @@ -397,12 +508,10 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return false; } - // called by compareDataTypeComponents() in order to compare the data - // types of the components + // called by compareDataTypeComponents() in order to compare the data types of the components private boolean compareDataTypes(DataType typeA, DataType typeB) { - // make sure the name and length are the same and then compare - // the data types recursively + // make sure the name and length are the same and then compare the data types recursively if (typeA instanceof Structure) { if (typeB instanceof Structure) { return compareStructuresByData((Structure) typeA, (Structure) typeB); @@ -445,13 +554,11 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { "Non-null structure is required when showing the Create Structure dialog."); } - // init the return value, which will be updated if the user presses - // the OK button + // init the return value, which will be updated if the user presses the OK button currentStructure = structure; nameTextField.setText(currentStructure.getName()); updateStatusText(true, currentStructure.getName()); - searchForMatchingStructures(program, structure); // modal block @@ -467,9 +574,6 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { super.cancelCallback(); } - /** - * The callback method for when the "OK" button is pressed. - */ @Override protected void okCallback() { @@ -477,6 +581,10 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { // just use the name set by the user String nameText = nameTextField.getText(); + if (!setCategoryPath()) { + return; + } + try { currentStructure.setName(nameText); } @@ -491,15 +599,87 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { } else { // get the selected object in the table - currentStructure = ((StructureWrapper) matchingStructuresTable - .getValueAt(matchingStructuresTable.getSelectedRow(), 0)).getStructure(); + currentStructure = getSelectedStructure(); } close(); } - // a table model that is used to allow for the easy updating of the - // table with new List data and to disable editing + private Structure getSelectedStructure() { + int row = matchingStructuresTable.getSelectedRow(); + if (row < 0) { + return null; + } + + Object cellValue = matchingStructuresTable.getValueAt(row, 0); + return ((StructureWrapper) cellValue).getStructure(); + } + + private boolean setCategoryPath() { + CategoryPath path = categoryPathEditor.getCellEditorValue(); + // First see if a category from the list was chosen and make sure the user didn't modify it. + // If they did, path needs to be parsed separately. + if (path != null && path.getPath().equals(categoryPathEditor.getCellEditorValueAsText())) { + try { + currentStructure.setCategoryPath(path); + } + catch (DuplicateNameException dne) { + setStatusText(dne.getMessage(), MessageType.ERROR); + return false; + } + return true; + } + + String categoryText = categoryPathEditor.getCellEditorValueAsText(); + // Selecting/entering a category is optional; root is default + if (!categoryText.isBlank()) { + try { + CategoryPath parsedPath = parseEnteredCategoryPath(categoryText); + currentStructure.setCategoryPath(parsedPath); + } + catch (DuplicateNameException dne) { + setStatusText(dne.getMessage(), MessageType.ERROR); + return false; + } + } + else { + try { + currentStructure.setCategoryPath(CategoryPath.ROOT); + } + catch (DuplicateNameException dne) { + setStatusText(dne.getMessage(), MessageType.ERROR); + return false; + } + } + return true; + } + + private CategoryPath parseEnteredCategoryPath(String categoryText) { + // entering a leading slash is optional, path is still generated accordingly + if (categoryText.startsWith(CategoryPath.DELIMITER_STRING)) { + return generateCategoryPath(categoryText.substring(1)); + } + return generateCategoryPath(categoryText); + } + + private CategoryPath generateCategoryPath(String categoryText) { + if (!categoryText.contains(CategoryPath.DELIMITER_STRING)) { + return new CategoryPath(CategoryPath.ROOT, categoryText); + } + + // Additional slashes need parsed as branch(es) and final leaf + List parts = split(categoryText); + return new CategoryPath(CategoryPath.ROOT, parts); + } + + private List split(String categoryText) { + List parts = new ArrayList( + Arrays.asList(categoryText.split(CategoryPath.DELIMITER_STRING))); + return parts; + } + + // a table model that is used to allow for the easy updating of the table with new List data + // and to disable editing /*package*/class StructureTableModel extends AbstractSortedTableModel { private List data = Collections.emptyList(); @@ -570,19 +750,17 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return; } - String message = null; + String prefix = EXISITING_STRUCTURE_STATUS_PREFIX; if (creatingNew) { - message = NEW_STRUCTURE_STATUS_PREFIX; - } - else { - message = EXISITING_STRUCTURE_STATUS_PREFIX; + prefix = NEW_STRUCTURE_STATUS_PREFIX; } - setStatusText("" + message + "
\"" + HTMLUtilities.escapeHTML(name) + "\""); + String escapeName = HTMLUtilities.escapeHTML(name); + String message = "%s'%s'".formatted(prefix, escapeName); + setStatusText(message); } - // this class is used instead of a cell renderer so that sorting will - // work on the table + // this class is used instead of a cell renderer so that sorting will work on the table /*package*/class StructureWrapper { private Structure structure; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java index a84032e2ea..8576090598 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java @@ -594,6 +594,18 @@ public class DataTypeManagerPlugin extends ProgramPlugin return dialog.getSelectedDataType(); } + @Override + public CategoryPath getCategoryPath(TreePath selectedPath) { + DataTypeChooserDialog dialog = new DataTypeChooserDialog(this); + dialog.setCategorySelectionMode(true); + + if (selectedPath != null) { + dialog.setSelectedPath(selectedPath); + } + tool.showDialog(dialog); + return dialog.getSelectedCategoryPath(); + } + @Override public DataTypeManager[] getDataTypeManagers() { return dataTypeManagerHandler.getDataTypeManagers(); @@ -664,6 +676,11 @@ public class DataTypeManagerPlugin extends ProgramPlugin return dataTypeManagerHandler.getDataTypeIndexer().getSortedDataTypeList(); } + @Override + public List getSortedCategoryPathList() { + return dataTypeManagerHandler.getDataTypeIndexer().getSortedCategoryPathList(); + } + @Override public void setDataTypeSelected(DataType dataType) { if (provider.isVisible()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeIndexer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeIndexer.java index 12cecfef57..d9c4ce1c0d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeIndexer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeIndexer.java @@ -23,10 +23,11 @@ import ghidra.program.model.data.*; import ghidra.util.task.*; /** - * A class that stores a sorted list of all the {@link DataType} objects in the current data type - * manager plugin. This class does its work lazily such that no work is done until - * {@link #getSortedDataTypeList()} is called. Even when that method is called no work will be - * done if the state of the data types in the system hasn't changed. + * A class that stores a sorted list of all the {@link DataType} and a list of all the + * {@link CategoryPath} objects in the current data type manager plugin. This class does its work + * lazily such that no work is done until {@link #getSortedDataTypeList()} is called. Even when that + * method is called no work will be done if the state of the data types in the system hasn't + * changed. */ public class DataTypeIndexer { private List dataTypeManagers = new ArrayList<>(); @@ -34,6 +35,7 @@ public class DataTypeIndexer { private DataTypeIndexUpdateListener listener = new DataTypeIndexUpdateListener(); private volatile boolean isStale = true; + private List categoryPathList = Collections.emptyList(); // Note: synchronizing here prevents concurrent mod issues with the managers list public synchronized void addDataTypeManager(DataTypeManager dataTypeManager) { @@ -79,6 +81,18 @@ public class DataTypeIndexer { return Collections.unmodifiableList(newList); } + /** + * Returns a list of the unique Category Paths ({@link CategoryPath}) as utilized by the + * data types open in the current tool. + * + * @return a list of the {@link CategoryPath} associated with the data types open in the + * current tool. + */ + public List getSortedCategoryPathList() { + updateDataTypeList(); // the category list is quietly updated in the background + return categoryPathList; + } + private List updateDataTypeList() { if (!isStale) { return dataTypeList; @@ -95,8 +109,9 @@ public class DataTypeIndexer { task.run(TaskMonitor.DUMMY); } - List newList = task.getList(); - return newList; + List newDataTypeList = task.getList(); + categoryPathList = task.getCategoryPathList(); + return newDataTypeList; } // Note: purposefully not synchronized for speed @@ -107,6 +122,7 @@ public class DataTypeIndexer { // is possible that once marked stale, we may never have another request for this data // again. dataTypeList = Collections.emptyList(); + categoryPathList = Collections.emptyList(); } //================================================================================================== @@ -155,7 +171,8 @@ public class DataTypeIndexer { private class IndexerTask extends Task { - private List list = new ArrayList<>(); + private List dataTypes = new ArrayList<>(); + private List categories; IndexerTask() { super("Data Type Indexer Task", false, true, true); @@ -169,15 +186,32 @@ public class DataTypeIndexer { for (DataTypeManager dataTypeManager : dataTypeManagers) { monitor.setMessage("Searching " + dataTypeManager.getName()); - dataTypeManager.getAllDataTypes(list); + dataTypeManager.getAllDataTypes(dataTypes); monitor.incrementProgress(1); } - Collections.sort(list, new CaseInsensitiveDataTypeComparator()); + Collections.sort(dataTypes, new CaseInsensitiveDataTypeComparator()); + populateCategoryList(dataTypes); + } + + private void populateCategoryList(List dataTypes) { + + Set set = new HashSet<>(); + for (DataType dt : dataTypes) { + CategoryPath path = dt.getCategoryPath(); + set.add(path); + } + + categories = new ArrayList<>(set); + Collections.sort(categories); } List getList() { - return list; + return dataTypes; + } + + List getCategoryPathList() { + return categories; } } @@ -266,4 +300,5 @@ public class DataTypeIndexer { markStale(); } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeChooserDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeChooserDialog.java index 38b32c82b9..4542ea9ef5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeChooserDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeChooserDialog.java @@ -34,10 +34,9 @@ import docking.widgets.filter.TextFilterStrategy; import docking.widgets.label.GLabel; import docking.widgets.tree.*; import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; -import ghidra.app.plugin.core.datamgr.tree.DataTypeArchiveGTree; -import ghidra.app.plugin.core.datamgr.tree.DataTypeNode; +import ghidra.app.plugin.core.datamgr.tree.*; import ghidra.app.util.datatype.DataTypeSelectionDialog; -import ghidra.program.model.data.DataType; +import ghidra.program.model.data.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -49,8 +48,12 @@ import ghidra.util.task.TaskMonitor; public class DataTypeChooserDialog extends DialogComponentProvider { private DataTypeArchiveGTree tree; private DataType selectedDataType; + private CategoryPath selectedCategoryPath; + private GLabel messageLabel; - boolean isFilterEditable; + private boolean isFilterEditable; + + private boolean categorySelectionMode; public DataTypeChooserDialog(DataTypeManagerPlugin plugin) { super("Data Type Chooser", true, true, true, false); @@ -60,7 +63,7 @@ public class DataTypeChooserDialog extends DialogComponentProvider { tree.setEditable(false); tree.updateFilterForChoosingDataType(); - tree.addGTreeSelectionListener(e -> setOkEnabled(getSelectedNode() != null)); + tree.addGTreeSelectionListener(e -> setOkEnabled(isValidNodeSelected())); tree.addMouseListener(new MouseAdapter() { @Override @@ -69,7 +72,18 @@ public class DataTypeChooserDialog extends DialogComponentProvider { return; } - DataTypeNode selectedNode = getSelectedNode(); + if (categorySelectionMode) { + CategoryPath path = getCurrentCategoryPath(); + if (path == null) { + return; + } + + selectedCategoryPath = path; + close(); + return; + } + + DataTypeNode selectedNode = getSelectedDtNode(); if (selectedNode == null) { return; } @@ -86,18 +100,48 @@ public class DataTypeChooserDialog extends DialogComponentProvider { setOkEnabled(false); } - private DataTypeNode getSelectedNode() { + /** + * Signals that this chooser is intended to pick {@link CategoryPath}s instead of data types. + * @param categorySelectionMode true to pick category paths + */ + public void setCategorySelectionMode(boolean categorySelectionMode) { + this.categorySelectionMode = categorySelectionMode; + } + + private boolean isValidNodeSelected() { + TreePath[] selectionPath = tree.getSelectionPaths(); + if (selectionPath.length != 1) { + return false; + } + + GTreeNode node = (GTreeNode) selectionPath[0].getLastPathComponent(); + return node instanceof DataTypeTreeNode; + } + + private DataTypeNode getSelectedDtNode() { TreePath[] selectionPath = tree.getSelectionPaths(); if (selectionPath.length != 1) { return null; } GTreeNode node = (GTreeNode) selectionPath[0].getLastPathComponent(); - if (!(node instanceof DataTypeNode)) { + if (node instanceof DataTypeNode dtNode) { + return dtNode; + } + return null; + } + + private CategoryNode getSelectedCategoryNode() { + TreePath[] selectionPath = tree.getSelectionPaths(); + if (selectionPath.length != 1) { return null; } - return (DataTypeNode) node; + GTreeNode node = (GTreeNode) selectionPath[0].getLastPathComponent(); + if (node instanceof CategoryNode catNode) { + return catNode; + } + return null; } @Override @@ -119,12 +163,36 @@ public class DataTypeChooserDialog extends DialogComponentProvider { @Override protected void okCallback() { - // can't be null since we control button enablement - DataTypeNode dataTypeNode = getSelectedNode(); - selectedDataType = dataTypeNode.getDataType(); + + if (categorySelectionMode) { + selectedCategoryPath = getCurrentCategoryPath(); + } + else { + DataTypeNode dtNode = getSelectedDtNode(); + selectedDataType = dtNode.getDataType(); + } + close(); } + private CategoryPath getCurrentCategoryPath() { + + DataTypeNode dtNode = getSelectedDtNode(); + + // the user may have picked a data type node or a category node + if (dtNode != null) { + return dtNode.getDataType().getCategoryPath(); + } + + CategoryNode categoryNode = getSelectedCategoryNode(); + if (categoryNode != null) { + Category category = categoryNode.getCategory(); + return category.getCategoryPath(); + } + + return null; + } + /** * A convenience method to show this dialog with the following configuration: *