diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png index c7d030078b..1b542fe2ad 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png and b/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png b/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png index be32a0fe22..e4bad542cd 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png and b/Ghidra/Features/Base/src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/data/CreateStructureCmd.java b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/data/CreateStructureCmd.java index 71e210c615..9b5069cc40 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/data/CreateStructureCmd.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/data/CreateStructureCmd.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. @@ -15,8 +15,7 @@ */ package ghidra.app.cmd.data; -import java.util.ArrayList; -import java.util.Collections; +import java.util.*; import ghidra.program.model.address.*; import ghidra.program.model.data.*; @@ -73,9 +72,6 @@ public class CreateStructureCmd extends AbstractCreateStructureCmd { structureDataLength = structure.getLength(); } - /* - * @see AbstractCreateStructureCmd#createStructure(Address, Program) - */ @Override Structure createStructure(Address address, Program program) { @@ -87,9 +83,6 @@ public class CreateStructureCmd extends AbstractCreateStructureCmd { return structure; } - /* - * @see AbstractCreateStructureCmd#initializeStructureData(Program, Structure) - */ @Override DataType initializeStructureData(Program program, Structure localStructure) { @@ -101,11 +94,11 @@ public class CreateStructureCmd extends AbstractCreateStructureCmd { } catch (AddressOverflowException e1) { throw new IllegalArgumentException( - "Can't create structure because length exceeds address " + "space" + + "Can't create structure because length exceeds address space" + structureDataLength); } ReferenceManager refMgr = program.getReferenceManager(); - Reference[] refs = findExistingRefs(refMgr, program.getAddressFactory(), + List refs = findExistingRefs(refMgr, program.getAddressFactory(), getStructureAddress(), endAddress); listing.clearCodeUnits(getStructureAddress(), endAddress, false); @@ -123,20 +116,20 @@ public class CreateStructureCmd extends AbstractCreateStructureCmd { return data.getDataType(); } - private Reference[] findExistingRefs(ReferenceManager refMgr, AddressFactory af, Address start, + private List findExistingRefs(ReferenceManager refMgr, AddressFactory af, + Address start, Address end) { - ArrayList list = new ArrayList(); + List list = new ArrayList(); AddressIterator it = refMgr.getReferenceSourceIterator(new AddressSet(start, end), true); while (it.hasNext()) { Address addr = it.next(); Reference[] refs = refMgr.getReferencesFrom(addr); Collections.addAll(list, refs); } - Reference[] refList = new Reference[list.size()]; - return list.toArray(refList); + return list; } - private void addRefs(Program p, ReferenceManager refMgr, Reference[] refs) { + private void addRefs(Program p, ReferenceManager refMgr, List refs) { for (Reference ref : refs) { refMgr.addReference(ref); } 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/compositeeditor/CompositeEditorPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java index 79a5bdedcb..2abce33b4a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorPanel.java @@ -1158,8 +1158,6 @@ public abstract class CompositeEditorPanel Swing.runLater(() -> stopEdit(tool))); + JButton browseButton = editor.getBrowseButton(); + browseButton.addActionListener(e -> Swing.runLater(() -> stopEdit(tool))); textField.addFocusListener(new FocusAdapter() { @Override @@ -1226,10 +1215,6 @@ public abstract class CompositeEditorPanel filterPanel; @@ -72,11 +74,13 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { * Creates a new dialog with the given parent. * * @param tool The current tool that this dialog needs to access services. + * @param program the current program */ - public CreateStructureDialog(PluginTool tool) { + public CreateStructureDialog(PluginTool tool, Program program) { super("Create Structure", true, true, true, false); - pluginTool = tool; + this.pluginTool = tool; + this.currentProgram = program; setHelpLocation(new HelpLocation("DataPlugin", "Create_Structure_Dialog")); addWorkPanel(buildMainPanel()); @@ -97,43 +101,79 @@ 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()); - nameTextField = new JTextField() { - // make sure our height doesn't stretch + 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(); + nameTextField.setName("StructureName"); + nameTextField.getAccessibleContext().setAccessibleName("Name"); + + // Allow user to click on the text field to re-activate "create new" panel without forcing + // a click on the radio button + nameTextField.addFocusListener(new FocusAdapter() { @Override - public Dimension getMaximumSize() { - Dimension d = super.getMaximumSize(); - d.height = getPreferredSize().height; - return d; - } - }; - nameTextField.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent event) { - setCreateStructureByName(true); - nameTextField.requestFocus(); + public void focusGained(FocusEvent e) { + 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 +193,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 +207,60 @@ 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); + JComponent editorComponent = categoryPathEditor.getEditorComponent(); + editorComponent.getAccessibleContext().setAccessibleName("Category"); + + categoryPathEditor.setCellEditorValue(CategoryPath.ROOT); + + // Allow user to click on the text field to re-activate "create new" panel without forcing + // a click on the radio button + 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(); + } + }); + ListSelectionModel selectionModel = table.getSelectionModel(); + selectionModel.addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) { + return; + } + + if (useExistingStructButton.isSelected()) { + setOkEnabled(table.getSelectedRowCount() > 0); + } + }); + filterPanel = new GhidraTableFilterPanel<>(table, structureTableModel) { // make sure our height doesn't stretch @Override @@ -190,14 +273,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 +298,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 +366,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 +419,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 +431,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 +445,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 +469,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 +483,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); @@ -418,6 +502,18 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { return false; } + JTextField getNameField() { + return nameTextField; + } + + JTable getTable() { + return matchingStructuresTable; + } + + CategoryPathSelectionEditor getCategoryEditor() { + return categoryPathEditor; + } + /** * Shows a dialog that allows the user to create a new structure. *

@@ -445,13 +541,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,39 +561,113 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { super.cancelCallback(); } - /** - * The callback method for when the "OK" button is pressed. - */ @Override protected void okCallback() { - if (nameTextField.isEnabled()) { - // just use the name set by the user - String nameText = nameTextField.getText(); - - try { - currentStructure.setName(nameText); - } - catch (InvalidNameException ine) { - setStatusText(ine.getMessage()); - return; - } - catch (DuplicateNameException dne) { - setStatusText(dne.getMessage()); - return; - } - } - else { + if (useExistingStructButton.isSelected()) { // get the selected object in the table - currentStructure = ((StructureWrapper) matchingStructuresTable - .getValueAt(matchingStructuresTable.getSelectedRow(), 0)).getStructure(); + currentStructure = getSelectedStructure(); + close(); + return; + } + + // just use the name set by the user + String nameText = nameTextField.getText(); + try { + currentStructure.setName(nameText); + } + catch (InvalidNameException ine) { + setStatusText(ine.getMessage()); + return; + } + catch (DuplicateNameException dne) { + setStatusText(dne.getMessage()); + return; + } + + if (!setCategoryPath()) { + return; + } + + if (!validateName()) { + return; } 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 boolean validateName() { + // Use the current name and category path to see if there is already an existing name. This + // allows us to avoid a conflict. + ProgramBasedDataTypeManager dtm = currentProgram.getDataTypeManager(); + CategoryPath path = currentStructure.getCategoryPath(); + Category category = dtm.getCategory(path); + if (category == null) { + return true; + } + + String nameText = currentStructure.getName(); + DataType existingDt = category.getDataType(nameText); + if (existingDt != null) { + setStatusText("Name already exists: " + nameText, MessageType.ERROR); + return false; + } + + return true; + } + + 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() { + + try { + doSetCategoryPath(); + } + catch (DuplicateNameException e) { + setStatusText(e.getMessage(), MessageType.ERROR); + return false; + } + return true; + } + + private void doSetCategoryPath() throws DuplicateNameException { + 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. + String editorValue = categoryPathEditor.getCellEditorValueAsText(); + if (path != null && path.getPath().equals(editorValue)) { + currentStructure.setCategoryPath(path); + return; + } + + // Selecting/entering a category is optional; root is default + if (!editorValue.isBlank()) { + CategoryPath parsedPath = parseEnteredCategoryPath(editorValue); + currentStructure.setCategoryPath(parsedPath); + return; + } + + currentStructure.setCategoryPath(CategoryPath.ROOT); + } + + private CategoryPath parseEnteredCategoryPath(String categoryText) { + // entering a leading slash is optional, path is still generated accordingly + if (categoryText.startsWith(CategoryPath.DELIMITER_STRING)) { + return new CategoryPath(categoryText); + } + return new CategoryPath(CategoryPath.DELIMITER_STRING + categoryText); + } + + // 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(); @@ -527,7 +695,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { case 0: return STRUCTURE_COLUMN_NAME; case 1: - return PATH_COLUMN_NAME; + return CATEGORY_COLUMN_NAME; } return null; } @@ -555,8 +723,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { case 1: { Structure structure = t.getStructure(); CategoryPath path = structure.getCategoryPath(); - String name = structure.getName(); - return path.toString() + '/' + name; + return path.toString(); } } return null; @@ -570,19 +737,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; @@ -601,7 +766,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { } // we need this renderer in order to create nice tool tip text values - class StructureCellRenderer extends GTableCellRenderer { + private class StructureCellRenderer extends GTableCellRenderer { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -621,7 +786,7 @@ public class CreateStructureDialog extends ReusableDialogComponentProvider { renderer.setToolTipText(ToolTipUtils.getToolTipText(structure)); } } - else if (PATH_COLUMN_NAME.equals(columnName)) { + else if (CATEGORY_COLUMN_NAME.equals(columnName)) { if (value != null) { renderer.setToolTipText(value.toString()); } 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..4d25970811 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,19 @@ public class DataTypeManagerPlugin extends ProgramPlugin return dialog.getSelectedDataType(); } + @Override + public CategoryPath getCategoryPath(TreePath selectedPath) { + DataTypeChooserDialog dialog = new DataTypeChooserDialog(this); + dialog.setCategorySelectionMode(true); + dialog.setShowProgramArchiveOnly(true); + + if (selectedPath != null) { + dialog.setSelectedPath(selectedPath); + } + tool.showDialog(dialog); + return dialog.getSelectedCategoryPath(); + } + @Override public DataTypeManager[] getDataTypeManagers() { return dataTypeManagerHandler.getDataTypeManagers(); @@ -664,6 +677,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..cc17249fce 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(); } //================================================================================================== @@ -153,9 +169,37 @@ public class DataTypeIndexer { } } + private class CaseInsensitiveCategoryComparator implements Comparator { + + @Override + public int compare(CategoryPath cp1, CategoryPath cp2) { + + String name1 = cp1.getName(); + String name2 = cp2.getName(); + + int result = name1.compareToIgnoreCase(name2); + if (result != 0) { + return result; + } + + result = name1.compareTo(name2); + if (result != 0) { + // let equivalent names be sorted by case ('-' for lower-case first) + return -result; + } + + // if the names are the same, then sort by full path + String p1 = cp1.getPath(); + String p2 = cp2.getPath(); + return p1.compareToIgnoreCase(p2); + } + } + private class IndexerTask extends Task { - private List list = new ArrayList<>(); + private List dataTypes = new ArrayList<>(); + private List categories = new ArrayList<>(); + private Set categorySet = new HashSet<>(); IndexerTask() { super("Data Type Indexer Task", false, true, true); @@ -167,17 +211,40 @@ public class DataTypeIndexer { monitor.initialize(dataTypeManagers.size()); monitor.setMessage("Preparing to index data types..."); - for (DataTypeManager dataTypeManager : dataTypeManagers) { - monitor.setMessage("Searching " + dataTypeManager.getName()); - dataTypeManager.getAllDataTypes(list); + for (DataTypeManager dtm : dataTypeManagers) { + monitor.setMessage("Searching " + dtm.getName()); + dtm.getAllDataTypes(dataTypes); + + Category root = dtm.getRootCategory(); + populateCategories(root); + monitor.incrementProgress(1); } - Collections.sort(list, new CaseInsensitiveDataTypeComparator()); + Collections.sort(dataTypes, new CaseInsensitiveDataTypeComparator()); + + categories.addAll(categorySet); + Collections.sort(categories, new CaseInsensitiveCategoryComparator()); + } + + private void populateCategories(Category parent) { + + categorySet.add(parent.getCategoryPath()); + Category[] children = parent.getCategories(); + for (Category category : children) { + CategoryPath path = category.getCategoryPath(); + categorySet.add(path); + + populateCategories(category); + } } List getList() { - return list; + return dataTypes; + } + + List getCategoryPathList() { + return categories; } } @@ -266,4 +333,5 @@ public class DataTypeIndexer { markStale(); } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java index 0f2c6ac04a..634ef9bd49 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java @@ -674,6 +674,13 @@ public class DataTypeManagerHandler { return builtInDataTypesManager; } + public DataTypeManager getProgramDataTypeManager() { + if (programArchive != null) { + return programArchive.getDataTypeManager(); + } + return null; + } + public DataTypeIndexer getDataTypeIndexer() { return dataTypeIndexer; } @@ -1810,4 +1817,5 @@ public class DataTypeManagerHandler { } return null; } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/ArchiveRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/ArchiveRootNode.java index 3d0a4e4537..ede28c2b21 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/ArchiveRootNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/ArchiveRootNode.java @@ -34,8 +34,15 @@ public class ArchiveRootNode extends DataTypeTreeNode { private DtFilterState dtFilterState = new DtFilterState(); - ArchiveRootNode(DataTypeManagerHandler archiveManager) { + private boolean programDtmOnly; + + public ArchiveRootNode(DataTypeManagerHandler archiveManager) { + this(archiveManager, false); + } + + public ArchiveRootNode(DataTypeManagerHandler archiveManager, boolean programDtmOnly) { this.archiveManager = archiveManager; + this.programDtmOnly = programDtmOnly; init(); } @@ -93,21 +100,29 @@ public class ArchiveRootNode extends DataTypeTreeNode { } // a factory method to isolate non-OO inheritance checks - private static final GTreeNode createArchiveNode(Archive archive, DtFilterState dtFilterState) { + private final GTreeNode createArchiveNode(Archive archive, DtFilterState filterState) { + + if (programDtmOnly) { + if (archive instanceof ProgramArchive) { + return new ProgramArchiveNode((ProgramArchive) archive, filterState); + } + return null; + } + if (archive instanceof FileArchive) { - return new FileArchiveNode((FileArchive) archive, dtFilterState); + return new FileArchiveNode((FileArchive) archive, filterState); } else if (archive instanceof ProjectArchive) { - return new ProjectArchiveNode((ProjectArchive) archive, dtFilterState); + return new ProjectArchiveNode((ProjectArchive) archive, filterState); } else if (archive instanceof InvalidFileArchive) { return new InvalidArchiveNode((InvalidFileArchive) archive); } else if (archive instanceof ProgramArchive) { - return new ProgramArchiveNode((ProgramArchive) archive, dtFilterState); + return new ProgramArchiveNode((ProgramArchive) archive, filterState); } else if (archive instanceof BuiltInArchive) { - return new BuiltInArchiveNode((BuiltInArchive) archive, dtFilterState); + return new BuiltInArchiveNode((BuiltInArchive) archive, filterState); } return null; } @@ -178,7 +193,10 @@ public class ArchiveRootNode extends DataTypeTreeNode { public List generateChildren() { List list = new ArrayList<>(); for (Archive element : archiveManager.getAllArchives()) { - list.add(createArchiveNode(element, dtFilterState)); + GTreeNode node = createArchiveNode(element, dtFilterState); + if (node != null) { + list.add(node); + } } Collections.sort(list); return list; @@ -221,15 +239,21 @@ public class ArchiveRootNode extends DataTypeTreeNode { @Override public void archiveOpened(Archive archive) { - if (isLoaded()) { - GTreeNode node = createArchiveNode(archive, dtFilterState); - List allChildrenList = getChildren(); - int index = Collections.binarySearch(allChildrenList, node); - if (index < 0) { - index = -index - 1; - } - addNode(index, node); + if (!isLoaded()) { + return; } + + GTreeNode node = createArchiveNode(archive, dtFilterState); + if (node == null) { + return; + } + + List allChildrenList = getChildren(); + int index = Collections.binarySearch(allChildrenList, node); + if (index < 0) { + index = -index - 1; + } + addNode(index, node); } @Override 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..873a022744 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 @@ -31,13 +31,14 @@ import docking.DialogComponentProvider; import docking.Tool; import docking.widgets.filter.FilterOptions; import docking.widgets.filter.TextFilterStrategy; -import docking.widgets.label.GLabel; +import docking.widgets.label.GDLabel; 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.archive.DataTypeManagerHandler; +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.HelpLocation; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -47,20 +48,27 @@ import ghidra.util.task.TaskMonitor; * {@link DataTypeSelectionDialog} utility widget. */ public class DataTypeChooserDialog extends DialogComponentProvider { + + private DataTypeManagerPlugin plugin; private DataTypeArchiveGTree tree; private DataType selectedDataType; - private GLabel messageLabel; - boolean isFilterEditable; + private CategoryPath selectedCategoryPath; + + private GDLabel messageLabel; + private boolean isFilterEditable; + + private boolean categorySelectionMode; public DataTypeChooserDialog(DataTypeManagerPlugin plugin) { super("Data Type Chooser", true, true, true, false); + this.plugin = plugin; tree = new DataTypeArchiveGTree(plugin); tree.setEditable(false); tree.updateFilterForChoosingDataType(); - tree.addGTreeSelectionListener(e -> setOkEnabled(getSelectedNode() != null)); + tree.addGTreeSelectionListener(e -> setOkEnabled(isValidNodeSelected())); tree.addMouseListener(new MouseAdapter() { @Override @@ -69,7 +77,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; } @@ -84,20 +103,75 @@ public class DataTypeChooserDialog extends DialogComponentProvider { addOKButton(); addCancelButton(); setOkEnabled(false); + + setHelpLocation(new HelpLocation("DataTypeEditors", "browse")); } - 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; + if (categorySelectionMode) { + setTitle("Category Chooser"); + messageLabel.setText("Choose a category:"); + } + else { + setTitle("Data Type Chooser"); + messageLabel.setText("Choose a data type:"); + } + } + + public void setShowProgramArchiveOnly(boolean programOnly) { + DataTypeManagerHandler handler = plugin.getDataTypeManagerHandler(); + if (programOnly) { + DataTypeManager programDtm = handler.getProgramDataTypeManager(); + if (programDtm != null) { + ArchiveRootNode root = new ArchiveRootNode(handler, true); + tree.setRootNode(root); + return; + } + } + + ArchiveRootNode root = new ArchiveRootNode(handler); + tree.setRootNode(root); + } + + 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 @@ -108,7 +182,7 @@ public class DataTypeChooserDialog extends DialogComponentProvider { private JComponent createWorkPanel() { JPanel panel = new JPanel(new BorderLayout()); - messageLabel = new GLabel("Choose the data type you wish to use."); + messageLabel = new GDLabel("Choose the data type you wish to use."); messageLabel.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 2)); messageLabel.getAccessibleContext().setAccessibleName("Message"); panel.add(messageLabel, BorderLayout.NORTH); @@ -119,12 +193,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 +305,10 @@ public class DataTypeChooserDialog extends DialogComponentProvider { tree.setFilterProvider(provider); } + public CategoryPath getSelectedCategoryPath() { + return selectedCategoryPath; + } + public DataType getSelectedDataType() { return selectedDataType; } @@ -263,4 +365,5 @@ public class DataTypeChooserDialog extends DialogComponentProvider { } } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterDataTypeCellEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterDataTypeCellEditor.java index e7603d7a6f..96c5581574 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterDataTypeCellEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterDataTypeCellEditor.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.function.editor; -import java.awt.*; +import java.awt.Component; import java.awt.event.*; import java.util.EventObject; @@ -32,16 +32,15 @@ import ghidra.app.util.datatype.DataTypeSelectionEditor; import ghidra.program.model.data.DataType; import ghidra.program.model.data.DataTypeManager; import ghidra.util.MessageType; +import ghidra.util.Swing; import ghidra.util.data.DataTypeParser; class ParameterDataTypeCellEditor extends AbstractCellEditor implements TableCellEditor, FocusableEditor { private DataTypeSelectionEditor editor; private DropDownSelectionTextField textField; - private JButton dataTypeChooserButton; private DataType dt; - private JPanel editorPanel; private DataTypeManagerService service; private DialogComponentProvider dialog; private DataTypeManager dtm; @@ -62,7 +61,7 @@ class ParameterDataTypeCellEditor extends AbstractCellEditor editor.setCellEditorValue(dt); - return editorPanel; + return editor.getEditorComponent(); } private void init() { @@ -83,17 +82,8 @@ class ParameterDataTypeCellEditor extends AbstractCellEditor } }); - // force a small button for the table's cell editor - dataTypeChooserButton = new JButton("...") { - @Override - public Dimension getPreferredSize() { - Dimension preferredSize = super.getPreferredSize(); - preferredSize.width = 15; - return preferredSize; - } - }; - - dataTypeChooserButton.addActionListener(e -> SwingUtilities.invokeLater(() -> { + JButton browseButton = editor.getBrowseButton(); + browseButton.addActionListener(e -> Swing.runLater(() -> { DataType dataType = service.getDataType((String) null); if (dataType != null) { editor.setCellEditorValue(dataType); @@ -104,18 +94,13 @@ class ParameterDataTypeCellEditor extends AbstractCellEditor } })); - FocusAdapter focusListener = new FocusAdapter() { + textField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { textField.selectAll(); textField.removeFocusListener(this); } - }; - textField.addFocusListener(focusListener); - - editorPanel = new JPanel(new BorderLayout()); - editorPanel.add(textField, BorderLayout.CENTER); - editorPanel.add(dataTypeChooserButton, BorderLayout.EAST); + }); } @Override @@ -136,7 +121,7 @@ class ParameterDataTypeCellEditor extends AbstractCellEditor * be returned if getTableCellEditorComponent method has not yet been invoked. */ public JButton getChooserButton() { - return dataTypeChooserButton; + return editor.getBrowseButton(); } @Override 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..183822a561 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java @@ -0,0 +1,467 @@ +/* ### + * 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.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.*; +import javax.swing.border.Border; +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; + +/** + * 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.setName("CategoryPath"); + selectionField.getAccessibleContext().setAccessibleName("Category"); + 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(); + } + }); + + JPanel browsePanel = buildBrowsePanel(); + editorPanel = new JPanel(); + editorPanel.setLayout(new BoxLayout(editorPanel, BoxLayout.X_AXIS)); + editorPanel.add(selectionField); + editorPanel.add(browsePanel); + + 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(); + } + } + }; + } + + private JPanel buildBrowsePanel() { + + // We override the various sizes to make sure the button does not get too big or too small, + // which changes depending upon the theme being used. + JPanel browsePanel = new JPanel() { + + @Override + public Dimension getPreferredSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + @Override + public Dimension getMinimumSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + @Override + public Dimension getMaximumSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + private int getBestWidth() { + Font f = getFont(); + FontMetrics fm = getFontMetrics(f); + int width = fm.stringWidth(" . . . "); + return width; + } + }; + + browsePanel.setLayout(new BorderLayout()); + browsePanel.setOpaque(false); + + // Space the button so that it pops out visually. This was chosen by trial-and-error and + // looks reasonable on all themes. + Border empty = BorderFactory.createEmptyBorder(2, 2, 1, 1); + browsePanel.setBorder(empty); + + browseButton = new BrowseButton(); + browseButton.setToolTipText("Browse Existing Category Paths"); + browseButton.addActionListener(e -> showBrowser()); + browsePanel.add(browseButton); + + return browsePanel; + } + + /** + * 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 drop-down text field that holds the category path collection. + * @return CategoryPath drop-down 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; + + public CategoryPathDropDownSelectionDataModel(DataTypeManagerService dataTypeService) { + data = dataTypeService.getSortedCategoryPathList(); + } + + @Override + public ListCellRenderer getListRenderer() { + return new CategoryPathDropDownRenderer(); + } + + @Override + public String getDescription(CategoryPath categoryPath) { + return null; + } + + @Override + public String getDisplayText(CategoryPath categoryPath) { + return categoryPath.getPath(); + } + + @Override + public List getMatchingData(String searchText) { + if (searchText == null || searchText.length() == 0) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + for (CategoryPath path : data) { + String pathString = path.getPath(); + if (pathString.contains(searchText)) { + results.add(path); + } + } + return results; + } + + @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 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/datatype/DataTypeSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java index e210d274c1..5dff65e417 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java @@ -15,9 +15,11 @@ */ package ghidra.app.util.datatype; +import java.awt.*; import java.awt.event.*; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.event.*; import javax.swing.tree.TreePath; @@ -141,15 +143,13 @@ public class DataTypeSelectionEditor extends AbstractCellEditor { selectionField.setBorder(UIManager.getBorder("Table.focusCellHighlightBorder")); - browseButton = new BrowseButton(); - browseButton.setToolTipText("Browse the Data Manager"); - browseButton.addActionListener(e -> showDataTypeBrowser()); + JPanel browsePanel = buildBrowsePanel(); editorPanel = new JPanel(); + editorPanel.setOpaque(false); editorPanel.setLayout(new BoxLayout(editorPanel, BoxLayout.X_AXIS)); editorPanel.add(selectionField); - editorPanel.add(Box.createHorizontalStrut(5)); - editorPanel.add(browseButton); + editorPanel.add(browsePanel); keyListener = new KeyAdapter() { @@ -171,6 +171,61 @@ public class DataTypeSelectionEditor extends AbstractCellEditor { }; } + private JPanel buildBrowsePanel() { + + // We override the various sizes to make sure the button does not get too big or too small, + // which changes depending upon the theme being used. + JPanel browsePanel = new JPanel() { + + @Override + public Dimension getPreferredSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + @Override + public Dimension getMinimumSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + @Override + public Dimension getMaximumSize() { + int width = getBestWidth(); + Dimension preferredSize = super.getPreferredSize(); + preferredSize.width = Math.min(width, preferredSize.width); + return preferredSize; + } + + private int getBestWidth() { + Font f = getFont(); + FontMetrics fm = getFontMetrics(f); + int width = fm.stringWidth(" . . . "); + return width; + } + }; + + browsePanel.setLayout(new BorderLayout()); + browsePanel.setOpaque(false); + + // Space the button so that it pops out visually. This was chosen by trial-and-error and + // looks reasonable on all themes. + Border empty = BorderFactory.createEmptyBorder(2, 2, 1, 1); + browsePanel.setBorder(empty); + + browseButton = new BrowseButton(); + browseButton.setToolTipText("Browse the Data Manager"); + browseButton.addActionListener(e -> showDataTypeBrowser()); + + browsePanel.add(browseButton); + + return browsePanel; + } + @Override public Object getCellEditorValue() { return selectionField.getSelectedValue(); 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/main/java/help/screenshot/AbstractScreenShotGenerator.java b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java index 82801196a6..6090e8304b 100644 --- a/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java +++ b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java @@ -1001,7 +1001,10 @@ public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIn public void selectRow(final JTable table, final int rowIndex) { waitForTable(table); - runSwing(() -> table.setRowSelectionInterval(rowIndex, rowIndex)); + runSwing(() -> { + table.setRowSelectionInterval(rowIndex, rowIndex); + table.requestFocus(); + }); waitForTable(table); } 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(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java index 9af89a2356..3f9e98234b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java @@ -974,7 +974,7 @@ public class DropDownTextField extends JTextField implements GComponent { fireUserChoiceMade(selectedItem); } - class PreviewListener implements ListSelectionListener { + private class PreviewListener implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/button/BrowseButton.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/button/BrowseButton.java index 7b5f424b85..7d58d000dc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/button/BrowseButton.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/button/BrowseButton.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. @@ -55,5 +55,7 @@ public class BrowseButton extends JButton { setIcon(ICON); setName(NAME); setToolTipText(TOOLTIP_TEXT); + + getAccessibleContext().setAccessibleName("Browse"); } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataPluginScreenShots.java index aa733c0e8c..ff012ae0e0 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataPluginScreenShots.java @@ -19,6 +19,9 @@ import java.util.Set; import java.util.stream.Collectors; import javax.swing.JRadioButton; +import javax.swing.JTable; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; import org.junit.Test; @@ -66,7 +69,21 @@ public class DataPluginScreenShots extends GhidraScreenShotGenerator { GhidraTable table = (GhidraTable) getInstanceField("matchingStructuresTable", dialog); selectRow(table, 2); - captureDialog(500, 400); + shrinkCategoryColumn(table); + + captureDialog(600, 500); + } + + private void shrinkCategoryColumn(JTable table) { + + runSwing(() -> { + TableColumnModel columnModel = table.getColumnModel(); + int columnIndex = columnModel.getColumnIndex("Category"); + TableColumn column = columnModel.getColumn(columnIndex); + int size = 150; + column.setPreferredWidth(size); + column.setMaxWidth(size); + }); } @Test