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: *
    @@ -207,6 +275,10 @@ public class DataTypeChooserDialog extends DialogComponentProvider { tree.setFilterProvider(provider); } + public CategoryPath getSelectedCategoryPath() { + return selectedCategoryPath; + } + public DataType getSelectedDataType() { return selectedDataType; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java index 4b5ba09682..1a26e37641 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java @@ -137,6 +137,15 @@ public interface DataTypeManagerService extends DataTypeQueryService, DataTypeAr */ public DataType getDataType(TreePath selectedPath); + /** + * Shows the user a dialog that allows them to choose a category path from a tree of all + * available categories. + * + * @param selectedPath An optional tree path to select in the tree + * @return A category path chosen by the user + */ + public CategoryPath getCategoryPath(TreePath selectedPath); + /** * Examines all enum dataTypes for items that match the given value. Returns a list of Strings * that might make sense for the given value. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeQueryService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeQueryService.java index ee3ac22f16..1ba4bb3624 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeQueryService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeQueryService.java @@ -40,6 +40,18 @@ public interface DataTypeQueryService { */ public List getSortedDataTypeList(); + /** + * Prompts the user for a data type. The optional filter text will be used to filter the tree + * of available types. + * Gets the sorted list of all category paths known by this service via its owned + * DataTypeManagers. This method can be called frequently, as the underlying data is indexed + * and only updated as changes are made. The sorting of the list is done using the + * natural sort of the {@link CategoryPath} objects. + * + * @return the sorted list of known category paths. + */ + public List getSortedCategoryPathList(); + /** * This method simply calls {@link #promptForDataType(String)} * @deprecated use {@link #promptForDataType(String)} @@ -49,8 +61,10 @@ public interface DataTypeQueryService { public DataType getDataType(String filterText); /** - * Prompts the user for a data type. The optional filter text will be used to filter the tree - * of available types. + * Obtain the preferred datatype which corresponds to the specified + * datatype specified by filterText. A tool-based service provider + * may prompt the user to select a datatype if more than one possibility + * exists. * * @param filterText If not null, this text filters the visible data types to only show those * that start with the given text diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java new file mode 100644 index 0000000000..228014a685 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java @@ -0,0 +1,466 @@ +/* ### + * 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.util.datatype; + +import java.awt.Component; +import java.awt.event.*; +import java.util.*; + +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.tree.TreePath; + +import docking.widgets.DropDownSelectionTextField; +import docking.widgets.DropDownTextFieldDataModel; +import docking.widgets.button.BrowseButton; +import docking.widgets.list.GListCellRenderer; +import ghidra.app.services.DataTypeManagerService; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.data.CategoryPath; +import ghidra.util.exception.AssertException; + +/** + * An editor that is used to show the {@link DropDownSelectionTextField} for the entering of + * category paths by name and offers the user of a completion window. This editor also provides a + * browse button that when pressed will show a data type tree so that the user may browse a tree + * of known category paths. + *

    + * Stand Alone Usage
    + * In order to use this component directly you need to call {@link #getEditorComponent()}. This + * will give you a Component for editing. + *

    + * In order to know when changes are made to the component you need to add a DocumentListener + * via the {@link #addDocumentListener(DocumentListener)} method. The added listener will be + * notified as the user enters text into the editor's text field. + */ +public class CategoryPathSelectionEditor extends AbstractCellEditor { + + private JPanel editorPanel; + private DropDownSelectionTextField selectionField; + private JButton browseButton; + private DataTypeManagerService dataTypeManagerService; + + private KeyAdapter keyListener; + private NavigationDirection navigationDirection; + + // optional path to initially select in the data type chooser tree + private TreePath initiallySelectedTreePath; + + /** + * Creates a new instance. + * + * @param serviceProvider {@link ServiceProvider} + */ + public CategoryPathSelectionEditor(ServiceProvider serviceProvider) { + + this.dataTypeManagerService = serviceProvider.getService(DataTypeManagerService.class); + + if (this.dataTypeManagerService == null) { + throw new NullPointerException("DataTypeManagerService cannot be null"); + } + init(); + } + + /** + * Sets whether this editor should consumer Enter key presses + * @see DropDownSelectionTextField#setConsumeEnterKeyPress(boolean) + * + * @param consume true to consume + */ + public void setConsumeEnterKeyPress(boolean consume) { + selectionField.setConsumeEnterKeyPress(consume); + } + + protected DropDownSelectionTextField createDropDownSelectionTextField( + CategoryPathDropDownSelectionDataModel model) { + return new DropDownSelectionTextField<>(model); + } + + private void init() { + selectionField = createDropDownSelectionTextField( + new CategoryPathDropDownSelectionDataModel(dataTypeManagerService)); + selectionField.addCellEditorListener(new CellEditorListener() { + @Override + public void editingCanceled(ChangeEvent e) { + fireEditingCanceled(); + navigationDirection = null; + } + + @Override + public void editingStopped(ChangeEvent e) { + fireEditingStopped(); + navigationDirection = null; + } + }); + + selectionField.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + selectionField.setEnabled(true); + selectionField.requestFocus(); + } + }); + selectionField.setBorder(UIManager.getBorder("Table.focusCellHighlightBorder")); + browseButton = new BrowseButton(); + browseButton.setToolTipText("Browse Existing Category Paths"); + browseButton.addActionListener(e -> showBrowser()); + + editorPanel = new JPanel(); + editorPanel.setLayout(new BoxLayout(editorPanel, BoxLayout.X_AXIS)); + editorPanel.add(selectionField); + editorPanel.add(Box.createHorizontalStrut(5)); + editorPanel.add(browseButton); + + keyListener = new KeyAdapter() { + + @Override + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + if (keyCode == KeyEvent.VK_TAB) { + if (e.isShiftDown()) { + navigationDirection = NavigationDirection.BACKWARD; + } + else { + navigationDirection = NavigationDirection.FORWARD; + } + + fireEditingStopped(); + e.consume(); + } + } + }; + } + + /** + * Retrieve the value in the cell. + * @return categoryPath of the selected value from the drop-down + */ + @Override + public CategoryPath getCellEditorValue() { + return selectionField.getSelectedValue(); + } + + /** + * If a path was selected from the drop-down list, it is already + * well-formed and cannot be null. + * @return the selected category path as CategoryPath + */ + public CategoryPath getCellEditorValueAsCategoryPath() { + return selectionField.getSelectedValue(); + } + + /** + * Returns the text value of the editor's text field. + * @return the text value of the editor's text field. + */ + public String getCellEditorValueAsText() { + return selectionField.getText(); + } + + /** + * Returns the component that allows the user to edit. + * @return the component that allows the user to edit. + */ + public JComponent getEditorComponent() { + return editorPanel; + } + + /** + * Retrieve the dropdown text field that holds the category path collection. + * @return CategoryPath dropdown selection text field object + */ + public DropDownSelectionTextField getDropDownTextField() { + return selectionField; + } + + /** + * The browse button which opens a menu with the Category Path collection from the data manager. + * @return browseButton + */ + public JButton getBrowseButton() { + return browseButton; + } + + /** + * Sets the initially selected node in the data type tree that the user can choose to + * show. + * + * @param path The path to set + */ + public void setDefaultSelectedTreePath(TreePath path) { + this.initiallySelectedTreePath = path; + } + + /** + * Place focus on the selectionField. + */ + public void requestFocus() { + selectionField.requestFocus(); + } + + /** + * Highlights the text of the cell editor. + */ + void selectCellEditorValue() { + selectionField.selectAll(); + } + + /** + * Sets the cell editor value as the entered String text. + * @param text String input + */ + public void setCellEditorValueAsText(String text) { + selectionField.setText(text); + navigationDirection = null; + } + + /** + * Sets the value to be edited on this cell editor. + * + * @param path The data type which is to be edited. + */ + public void setCellEditorValue(CategoryPath path) { + selectionField.setSelectedValue(path); + navigationDirection = null; + } + + /** + * Adds a document listener to the text field editing component of this editor so that users + * can be notified when the text contents of the editor change. + * @param listener the listener to add. + */ + public void addDocumentListener(DocumentListener listener) { + selectionField.getDocument().addDocumentListener(listener); + } + + /** + * Removes a previously added document listener. + * @param listener the listener to remove. + */ + public void removeDocumentListener(DocumentListener listener) { + selectionField.getDocument().removeDocumentListener(listener); + } + + /** + * Add the provided FocusListener to the selectionField. + * @param listener FocusListener + */ + public void addFocusListener(FocusListener listener) { + selectionField.addFocusListener(listener); + } + + /** + * Remove the provided FocusListener from the selectionField. + * @param listener FocusListener + */ + public void removeFocusListener(FocusListener listener) { + selectionField.removeFocusListener(listener); + } + + /** + * Toggle Tab key commits an edit. Sets the traversal key enabled field of the selectionField. + * @param doesCommit Boolean + */ + public void setTabCommitsEdit(boolean doesCommit) { + selectionField.setFocusTraversalKeysEnabled(!doesCommit); + + removeKeyListener(keyListener); // always remove to prevent multiple additions + if (doesCommit) { + addKeyListener(keyListener); + } + } + + /** + * Returns the direction of the user triggered navigation; null if the user did not trigger + * navigation out of this component. + * @return the direction + */ + public NavigationDirection getNavigationDirection() { + return navigationDirection; + } + + private void addKeyListener(KeyListener listener) { + selectionField.addKeyListener(listener); + } + + private void removeKeyListener(KeyListener listener) { + selectionField.removeKeyListener(listener); + } + + private void showBrowser() { + CategoryPath path = dataTypeManagerService.getCategoryPath(initiallySelectedTreePath); + if (path != null) { + setCellEditorValue(path); + selectionField.requestFocus(); + } + } + + /** + * Enable or disable the Category Path Text Field. + * @param createStructureByName Boolean + */ + public void setEnabled(boolean createStructureByName) { + selectionField.setEnabled(createStructureByName); + } + + /** + * Determine whether the Category Path Text Field is enabled. + * @return isEnabled boolean + */ + public boolean isEnabled() { + return selectionField.isEnabled(); + } + + /** + * CategoryPathDropDownSelectionDataModel class handles the display and selection of a + * Category Path. + */ + private class CategoryPathDropDownSelectionDataModel + implements DropDownTextFieldDataModel { + + private List data; + + private Comparator searchComparator = new CategoryPathComparator(); + + /** + * Creates a new instance. + * + * @param dataTypeService {@link DataTypeManagerService} + */ + public CategoryPathDropDownSelectionDataModel(DataTypeManagerService dataTypeService) { + data = dataTypeService.getSortedCategoryPathList(); + } + + @Override + public ListCellRenderer getListRenderer() { + return new CategoryPathDropDownRenderer(); + } + + /** + * Description of the CategoryPath is the display text of the path as a string. + + * @param categoryPath CategoryPath + * @return String representation of the Category Path + */ + @Override + public String getDescription(CategoryPath categoryPath) { + return getDisplayText(categoryPath); + } + + /** + * Retrieve the CategoryPath string representation. + * + * @param categoryPath CategoryPath + * @return String representation of the Category Path + */ + @Override + public String getDisplayText(CategoryPath categoryPath) { + return categoryPath.getPath(); + } + + /** + * Support for the filtering mechanism on the collection of Category Paths in the Data Manager. + * + * @param searchText String entered text + * @return filtered list of Category Paths + */ + @Override + public List getMatchingData(String searchText) { + if (searchText == null || searchText.length() == 0) { + return Collections.emptyList(); + } + + char END_CHAR = '\uffff'; + String searchTextStart = searchText; + String searchTextEnd = searchText + END_CHAR; + + int startIndex = Collections.binarySearch(data, searchTextStart, searchComparator); + int endIndex = Collections.binarySearch(data, searchTextEnd, searchComparator); + + // the binary search returns a negative, incremented position if there is no match in the + // list for the given search + if (startIndex < 0) { + startIndex = -startIndex - 1; + } + + if (endIndex < 0) { + endIndex = -endIndex - 1; + } + + return data.subList(startIndex, endIndex); + } + + /** + * Identify index of first matching CategoryPath from entered text string. + * @param dataCollection list of Category Paths + * @param text search string + * @return int index of first match + */ + @Override + public int getIndexOfFirstMatchingEntry(List dataCollection, String text) { + int lastPreferredMatchIndex = -1; + for (int i = 0; i < data.size(); i++) { + CategoryPath dataType = data.get(i); + String dataTypeName = dataType.getName(); + dataTypeName = dataTypeName.replaceAll(" ", ""); + if (dataTypeName.equals(text)) { + // an exact match is the best possible match! + return i; + } + + if (dataTypeName.equalsIgnoreCase(text)) { + // keep going, but remember this location, in case we don't find any more matches + lastPreferredMatchIndex = i; + } + else { + // we've encountered a non-matching entry--nothing left to search + return lastPreferredMatchIndex; + } + } + + return -1; // we only get here when the list is empty + } + + private class CategoryPathComparator implements Comparator { + @Override + public int compare(Object o1, Object o2) { + if (o1 instanceof CategoryPath && o2 instanceof String) { + CategoryPath path = (CategoryPath) o1; + return path.getName().compareToIgnoreCase(((String) o2)); + } + throw new AssertException( + "CategoryPathCompartor used to compare files against a String key!"); + } + } + + private class CategoryPathDropDownRenderer extends GListCellRenderer { + + @Override + protected String getItemText(CategoryPath path) { + return path.getPath(); + } + + @Override + public Component getListCellRendererComponent(JList list, + CategoryPath value, int index, boolean isSelected, boolean cellHasFocus) { + + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + return this; + } + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionDialog.java index 80b68433f2..bb78198863 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionDialog.java @@ -22,7 +22,7 @@ import javax.swing.JPanel; import javax.swing.event.*; import docking.DialogComponentProvider; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.data.DataType; import ghidra.program.model.data.DataTypeManager; import ghidra.util.HelpLocation; @@ -36,17 +36,17 @@ import ghidra.util.data.DataTypeParser.AllowedDataTypes; public class DataTypeSelectionDialog extends DialogComponentProvider { private DataTypeSelectionEditor editor; - private PluginTool pluginTool; + private ServiceProvider serviceProvider; private DataType userChoice; private int maxSize = -1; private DataTypeManager dtm; private final AllowedDataTypes allowedTypes; - public DataTypeSelectionDialog(PluginTool pluginTool, DataTypeManager dtm, int maxSize, - DataTypeParser.AllowedDataTypes allowedTypes) { + public DataTypeSelectionDialog(ServiceProvider serviceProvider, DataTypeManager dtm, + int maxSize, DataTypeParser.AllowedDataTypes allowedTypes) { super("Data Type Chooser Dialog", true, true, true, false); - this.pluginTool = pluginTool; + this.serviceProvider = serviceProvider; this.dtm = dtm; this.maxSize = maxSize; this.allowedTypes = allowedTypes; @@ -65,7 +65,7 @@ public class DataTypeSelectionDialog extends DialogComponentProvider { private void buildEditor() { removeWorkPanel(); - editor = createEditor(pluginTool, allowedTypes); + editor = createEditor(serviceProvider, allowedTypes); editor.setConsumeEnterKeyPress(false); // we want to handle Enter key presses editor.addCellEditorListener(new CellEditorListener() { @Override @@ -108,9 +108,9 @@ public class DataTypeSelectionDialog extends DialogComponentProvider { rootPanel.validate(); } - protected DataTypeSelectionEditor createEditor(PluginTool tool, + protected DataTypeSelectionEditor createEditor(ServiceProvider sp, AllowedDataTypes allowedDataTypes) { - return new DataTypeSelectionEditor(dtm, tool, allowedDataTypes); + return new DataTypeSelectionEditor(dtm, sp, allowedDataTypes); } protected JComponent createEditorPanel(DataTypeSelectionEditor dtEditor) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/parser/FunctionSignatureParser.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/parser/FunctionSignatureParser.java index 63f631d3ce..d90cb4429b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/parser/FunctionSignatureParser.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/parser/FunctionSignatureParser.java @@ -352,6 +352,11 @@ public class FunctionSignatureParser { return service.getSortedDataTypeList(); } + @Override + public List getSortedCategoryPathList() { + return service.getSortedCategoryPathList(); + } + @Override public DataType getDataType(String filterText) { return promptForDataType(filterText); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/datatype/DataTypeSelectionDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/datatype/DataTypeSelectionDialogTest.java index 6508cf43c1..26b44258bf 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/datatype/DataTypeSelectionDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/datatype/DataTypeSelectionDialogTest.java @@ -56,6 +56,7 @@ import ghidra.app.services.DataTypeManagerService; import ghidra.app.services.ProgramManager; import ghidra.framework.Application; import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.database.ProgramBuilder; import ghidra.program.database.data.ProgramDataTypeManager; import ghidra.program.model.data.*; @@ -123,9 +124,9 @@ public class DataTypeSelectionDialogTest extends AbstractGhidraHeadedIntegration dialog = new DataTypeSelectionDialog(tool, program.getDataTypeManager(), -1, AllowedDataTypes.ALL) { @Override - protected DataTypeSelectionEditor createEditor(PluginTool pluginTool, + protected DataTypeSelectionEditor createEditor(ServiceProvider sp, AllowedDataTypes allowedDataTypes) { - return new DataTypeSelectionEditor(null, pluginTool, allowedDataTypes) { + return new DataTypeSelectionEditor(null, sp, allowedDataTypes) { @Override protected DropDownSelectionTextField createDropDownSelectionTextField( diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java index e092b68769..e0971a51ca 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java @@ -50,7 +50,7 @@ public class TestDoubleDataTypeManagerService implements DataTypeManagerService } @Override - public DataType getDataType(String filterText) { + public List getSortedCategoryPathList() { throw new UnsupportedOperationException(); } @@ -161,11 +161,21 @@ public class TestDoubleDataTypeManagerService implements DataTypeManagerService throw new UnsupportedOperationException(); } + @Override + public DataType getDataType(String filterText) { + throw new UnsupportedOperationException(); + } + @Override public DataType getDataType(TreePath selectedPath) { throw new UnsupportedOperationException(); } + @Override + public CategoryPath getCategoryPath(TreePath selectedPath) { + throw new UnsupportedOperationException(); + } + @Override public Set getPossibleEquateNames(long value) { throw new UnsupportedOperationException();