GT-3054: Updated function tag panel to include table showing all

functions using a tag
This commit is contained in:
adamopolous 2019-08-12 10:26:30 -04:00
parent 4496e51b0b
commit bddb1a5518
21 changed files with 1111 additions and 370 deletions

View file

@ -400,7 +400,7 @@ src/main/help/help/topics/FunctionTagPlugin/images/DeleteWarning.png||GHIDRA||||
src/main/help/help/topics/FunctionTagPlugin/images/EditNotAllowedWarning.png||GHIDRA||||END|
src/main/help/help/topics/FunctionTagPlugin/images/EditTag.png||GHIDRA||||END|
src/main/help/help/topics/FunctionTagPlugin/images/FilterField.png||GHIDRA||||END|
src/main/help/help/topics/FunctionTagPlugin/images/FunctionTagPlugin.png||GHIDRA||||END|
src/main/help/help/topics/FunctionTagPlugin/images/FullWindow.png||GHIDRA||||END|
src/main/help/help/topics/FunctionTagPlugin/images/InputField.png||GHIDRA||||END|
src/main/help/help/topics/FunctionWindowPlugin/function_window.htm||GHIDRA||||END|
src/main/help/help/topics/FunctionWindowPlugin/images/FunctionWindow.png||GHIDRA||||END|

View file

@ -25,7 +25,7 @@
<TABLE border="0" width="100%">
<TR>
<TD width="100%" align="center"><IMG alt="" border="0" src=
"images/FunctionTagPlugin.png"></TD>
"images/FullWindow.png"></TD>
</TR>
</TABLE>
</CENTER>
@ -33,24 +33,24 @@
<H2>Window Components</H2>
<BLOCKQUOTE>
<P>This window has five distinct sections:</P>
<UL>
<LI><B>Available Tags List</B>: Displays all tags that are available to assign to the
current function.</LI>
<LI><B>All Tags TAble</B>: Displays all tags that are available to assign to the
current function, as well as a count of the number of times each tag has been used
in the current program.</LI>
<LI><B>Assigned Tags List</B>: Displays all tags that have been assigned to the current
function.</LI>
<LI><B>Function Panel</B>: Displays functions that contain any selected tags. If multiple
tags are selected, functions containing ANY of the tags will be displayed.</LI>
<LI>
<B>Tag Input Field</B>: Allows users to create new tags. Multiple tags may be created at one time.
<P style="margin: 20px;"><IMG alt="" border="0" src="images/InputField.png"></P>
</LI>
<LI>
<B>Filter Field</B>: Allows users to filter what is shown in the Available and Assigned tag lists.
<P style="margin: 20px;"><IMG alt="" border="0" src="images/FilterField.png"></P>
</LI>
</UL>
@ -58,10 +58,10 @@
<UL class="noindent">
<LI><B>Action Buttons</B></LI>
<UL>
<LI><IMG alt="" border="0" src="images/2rightarrow.png"> &nbsp;
<LI><IMG alt="" border="0" src="images/2rightarrow.png" width="16" height="16"> &nbsp;
Assigns the selected tag(s) to the current function</LI>
<LI><IMG alt="" border="0" src="images/2leftarrow.png"> &nbsp;
<LI><IMG alt="" border="0" src="images/2leftarrow.png" width="16" height="16"> &nbsp;
Removes the selected tag(s) from the current function</LI>
<LI><IMG alt="" border="0" src="Icons.DELETE_ICON"> &nbsp;
@ -80,7 +80,7 @@
<BLOCKQUOTE>
<P>Tags can be created by using the <I>Tag Input Field</I> described above. Users may enter
multiple tag names delimited by a comma. All newly-created tags will be displayed in the
Available Tags List and are NOT assigned to any function.</P>
Available Tags List but are NOT yet assigned to any function.</P>
<P><IMG alt="" border="0" src="../../shared/tip.png">Each tag may have an associated comment that
is visible as a tooltip. This can be assigned after the tag has been created (see <code>edit</code> section below).</P>
@ -112,6 +112,14 @@
<P>If editing is allowed, the following dialog will be shown:</P>
<P align="center"><IMG alt="" border="0" src="images/EditTag.png"></P>
<P><IMG alt="" border="0" src="../../shared/tip.png">An <i>italicized</i> tag name
indicates that the tag was loaded from an external source and has not yet been
added to the program, making it immutable. As soon as the tag is assigned to a function it becomes
editable. If you delete the tag using the <IMG alt="" border="0" src=
"Icons.DELETE_ICON" width="16" height="16"> icon it will be removed from
the program and once again be immutable.</P>
</BLOCKQUOTE>
<H3><A name="Add_Tag_To_Function"></A>Add to Function</H3>
@ -119,7 +127,7 @@
<BLOCKQUOTE>
<P>Tags may be added to a function by selecting a set of tags and pressing the <IMG alt=""
border="0" src="images/2rightarrow.png" width="16" height="16"> button. The tags will be
removed from the Available Tags List and added to the Assigned Tags List.</P>
added to the Assigned Tags list, and shown as disabled in the Available Tags list.</P>
</BLOCKQUOTE>
<H3><A name="Remove_Tag_From_Function"></A>Remove from Function</H3>
@ -134,10 +142,12 @@
<H2><a name="loading_tags">Loading External Tags</a></H2>
<BLOCKQUOTE>
<P>Tags may be loaded on startup from an external source if desired. These will be shown in
<FONT color="blue">blue</FONT> and cannot be edited or deleted, with one caveat: once a tag has
been assigned to a function it ceases to have any special protections and can be edited/deleted
just as any other.</P>
<P>Tags may be loaded on startup from an external source if desired. These tags will be shown with
an asterisk (*) after the name and cannot be edited or deleted; with one caveat: once a tag has
been assigned to a function it ceases to have any special protections and can be edited
like any other. If the tag is ever removed from all functions using the
<IMG alt="" border="0" src="Icons.DELETE_ICON"> button, it will again be present in the
Available Tags list.</P>
<P>To make these available there must be a file named <I>functionTags.xml</I> available on
the classpath. Edit (or create) this file and add tags as needed. The format is

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

@ -0,0 +1,127 @@
/* ###
* 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.plugin.core.function.tags;
import java.awt.BorderLayout;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import ghidra.app.services.GoToService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionTag;
import ghidra.program.model.listing.Program;
import ghidra.util.table.GhidraThreadedTablePanel;
/**
* Displays all functions that are associated with the selected tag in the
* {@link SourceTagsPanel}
*/
public class AllFunctionsPanel extends JPanel {
private FunctionTableModel model;
private JLabel titleLabel;
/**
* Constructor
*
* @param program the current program
* @param provider the component provider
* @param title the title of the panel
*/
public AllFunctionsPanel(Program program, ComponentProviderAdapter provider, String title) {
model = new FunctionTableModel(title, provider.getTool(), program, null);
GhidraThreadedTablePanel<Function> tablePanel = new GhidraThreadedTablePanel<>(model);
setLayout(new BorderLayout());
titleLabel = new JLabel(title);
titleLabel.setBorder(BorderFactory.createEmptyBorder(3, 5, 0, 0));
add(titleLabel, BorderLayout.NORTH);
add(tablePanel, BorderLayout.CENTER);
GoToService goToService = provider.getTool().getService(GoToService.class);
if (goToService != null) {
tablePanel.getTable().installNavigation(goToService, goToService.getDefaultNavigatable());
}
}
/**
* Updates the table with whatever is in the {@link #model}
*/
public void refresh() {
model.fireTableDataChanged();
}
/**
* Updates the table with functions containing the selected tags given
*
* @param selectedTags the selected function tags
*/
public void refresh(List<FunctionTag> selectedTags) {
setSelectedTags(selectedTags);
}
/**
* Stores the current program
*
* @param program the current program
*/
public void setProgram(Program program) {
model.setProgram(program);
}
/**
* Updates the panel with the set of tags selected by the user. This
* will update the panel title and the contents of the function table.
*
* @param tags the selected tags
*/
public void setSelectedTags(List<FunctionTag> tags) {
String tagNames = tags.stream()
.map(t -> t.getName())
.collect(Collectors.joining(" or "))
.toString();
titleLabel.setText("Functions With Tag: " + tagNames);
model.setSelectedTags(tags);
}
/**
* Returns the list of functions in the panel
* <p>
* This is only used for testing!
*
* @return the list of functions
*/
public List<Function> getFunctions() {
return model.getFunctions();
}
/**
* Returns the functions table model
*
* @return the functions table model
*/
public FunctionTableModel getTableModel() {
return model;
}
}

View file

@ -0,0 +1,120 @@
/* ###
* 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.plugin.core.function.tags;
import java.util.List;
import docking.widgets.table.DiscoverableTableUtils;
import docking.widgets.table.TableColumnDescriptor;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.AddressBasedTableModel;
import ghidra.util.table.field.*;
import ghidra.util.task.TaskMonitor;
/**
* The data model that backs the {@link AllFunctionsPanel}. This displays a list
* of functions that have function tags matching a provided set. Note that
* a function will be displayed as long as it has AT LEAST ONE of the tags
* in the set.
*/
class FunctionTableModel extends AddressBasedTableModel<Function> {
// The function tags to display functions for
private List<FunctionTag> tags;
/**
* Constructor
*
* @param title the title of the model
* @param serviceProvider the service provider
* @param program the current program
* @param monitor the task monitor
*/
public FunctionTableModel(String title, ServiceProvider serviceProvider, Program program,
TaskMonitor monitor) {
super(title, serviceProvider, program, monitor);
}
@Override
public Address getAddress(int row) {
Function rowObject = getRowObject(row);
return rowObject.getEntryPoint();
}
@Override
protected TableColumnDescriptor<Function> createTableColumnDescriptor() {
TableColumnDescriptor<Function> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new LabelTableColumn()));
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new AddressTableColumn()), 1, true);
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new FunctionTagTableColumn()));
return descriptor;
}
@Override
protected void doLoad(Accumulator<Function> accumulator, TaskMonitor monitor)
throws CancelledException {
if (program == null) {
return;
}
if (tags == null) {
return;
}
// Loop over all functions in the program, filtering out those that do not
// do not contain at least one of the tags in the provided set.
FunctionIterator iter = program.getFunctionManager().getFunctions(true);
monitor.initialize(program.getFunctionManager().getFunctionCount());
while (iter.hasNext()) {
monitor.incrementProgress(1);
monitor.checkCanceled();
Function f = iter.next();
boolean hasTag = f.getTags().stream().anyMatch(t -> tags.contains(t));
if (hasTag) {
accumulator.add(f);
}
}
}
/**
* Sets the tags associated with this model. This causes a reload of
* the function table
*
* @param tags the selected tags
*/
public void setSelectedTags(List<FunctionTag> tags) {
this.tags = tags;
reload();
}
/**
* Returns the list of functions in the table
*
* @return the contents of the table
*/
public List<Function> getFunctions() {
return getAllData();
}
}

View file

@ -15,8 +15,7 @@
*/
package ghidra.app.plugin.core.function.tags;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.*;
import java.awt.event.ActionListener;
import javax.swing.*;
@ -70,8 +69,9 @@ public class FunctionTagButtonPanel extends JPanel {
boolean hasSelection = sourcePanel.hasSelection();
boolean isImmutable = sourcePanel.isSelectionImmutable();
boolean isEnabled = sourcePanel.isSelectionEnabled();
addBtn.setEnabled(hasSelection && validFunction);
addBtn.setEnabled(hasSelection && validFunction && isEnabled);
removeBtn.setEnabled(false);
if (!hasSelection) {
@ -112,7 +112,9 @@ public class FunctionTagButtonPanel extends JPanel {
gbc.gridx = 0;
gbc.gridy = 0;
addBtn = createButton("addBtn", ADD_IMG, "Add selected tags to the function",
e -> sourcePanel.addSelectedTags());
e -> {
sourcePanel.addSelectedTags();
});
add(addBtn, gbc);
gbc.gridy = 1;
@ -127,6 +129,9 @@ public class FunctionTagButtonPanel extends JPanel {
targetPanel.deleteSelectedTags();
});
add(deleteBtn, gbc);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setMaximumSize(new Dimension(30, 300));
}
/**

View file

@ -1,93 +0,0 @@
/* ###
* 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.plugin.core.function.tags;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseEvent;
import javax.swing.*;
import docking.widgets.list.GListCellRenderer;
import ghidra.program.model.listing.FunctionTag;
import ghidra.util.HTMLUtilities;
/**
* Simple list for displaying {@link FunctionTag} items. The only part of the tag
* displayed in the list is the name attribute.
*/
public class FunctionTagList extends JList<FunctionTag> {
public FunctionTagList(DefaultListModel<FunctionTag> model) {
super(model);
setCellRenderer(getCellRenderer());
}
@Override
public String getToolTipText(MouseEvent evt) {
int index = locationToIndex(evt.getPoint());
if (index == -1 || index >= getModel().getSize()) {
return "";
}
Object obj = getModel().getElementAt(index);
if (obj instanceof FunctionTag) {
FunctionTag tag = (FunctionTag) obj;
if (tag.getComment().isEmpty()) {
return "<no comment set>";
}
return "<html>" + HTMLUtilities.escapeHTML(tag.getComment());
}
return "";
}
/**
* Custom renderer for this list ensures that we only show the name attribute of
* each {@link FunctionTag} object in the list.
*
* @return the cell renderer
*/
@Override
public ListCellRenderer<? super FunctionTag> getCellRenderer() {
return new GListCellRenderer<>() {
@Override
protected String getItemText(FunctionTag value) {
return value.getName();
}
@Override
public Component getListCellRendererComponent(JList<? extends FunctionTag> list,
FunctionTag value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
// If this tag is a temporary one (ie: read-in from a file), then it is
// read-only and should be indicated to the user as a different color.
if (value instanceof FunctionTagTemp) {
if (cellHasFocus) {
setForeground(Color.white);
}
else {
setForeground(Color.blue);
}
}
return this;
}
};
}
}

View file

@ -18,7 +18,8 @@ package ghidra.app.plugin.core.function.tags;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
@ -58,6 +59,11 @@ public class FunctionTagPlugin extends ProgramPlugin {
* PUBLIC METHODS
******************************************************************************/
/**
* Returns the component provider for this plugin
*
* @return the component provider
*/
public FunctionTagsComponentProvider getProvider() {
return provider;
}

View file

@ -0,0 +1,125 @@
/* ###
* 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.plugin.core.function.tags;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.MouseEvent;
import java.util.Set;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionTag;
import ghidra.util.HTMLUtilities;
import ghidra.util.table.GhidraTable;
/**
* Table that displays function tags and a count of the number of times
* each tag has been used
*/
public class FunctionTagTable extends GhidraTable {
/**
* If true, disable any rows that have already been assigned
* to a function (and thus cannot be added again)
*/
private boolean disable = false;
/** The selected function */
private Function function = null;
/**
* Constructor
*
* @param model the table model
*/
public FunctionTagTable(FunctionTagTableModel model) {
super(model);
}
protected void setDisabled(boolean disable) {
this.disable = disable;
}
public void setFunction(Function function) {
this.function = function;
}
@Override
public String getToolTipText(MouseEvent evt) {
FunctionTagTable table = (FunctionTagTable) evt.getSource();
int row = this.rowAtPoint(evt.getPoint());
int nameCol = table.getColumnModel().getColumnIndex("Name");
String tagName = (String)table.getValueAt(row, nameCol);
FunctionTagTableModel model = (FunctionTagTableModel) table.getModel();
FunctionTag tag = model.getTag(tagName);
if (tag.getComment().isEmpty()) {
return "no tooltip set";
}
return "<html>" + HTMLUtilities.escapeHTML(tag.getComment());
}
/**
* We need to override the renderer for the following cases:
* <li>italicize tags that cannot be edited</li>
* <li>disable rows in the source table that have already been added to the selected function </li>
*/
@Override
public TableCellRenderer getCellRenderer(int row, int col) {
return new TableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();
Component c = renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
boolean enableRow = true;
if (disable && function != null) {
int nameCol = table.getColumnModel().getColumnIndex("Name");
String nameVal = (String)table.getValueAt(row, nameCol);
Set<FunctionTag> tags = function.getTags();
enableRow = !tags.stream().anyMatch(t -> t.getName().equals(nameVal));
}
c.setEnabled(enableRow);
switch (table.getColumnName(column)) {
case "Count":
break;
case "Name":
FunctionTagTableModel model = (FunctionTagTableModel) table.getModel();
FunctionTag tag = model.getTag((String)value);
if (tag instanceof FunctionTagTemp) {
c.setFont(getFont().deriveFont(Font.ITALIC));
}
else {
c.setFont(getFont().deriveFont(Font.PLAIN));
}
break;
}
return c;
}
};
}
}

View file

@ -0,0 +1,207 @@
/* ###
* 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.plugin.core.function.tags;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import docking.widgets.table.AbstractDynamicTableColumnStub;
import docking.widgets.table.TableColumnDescriptor;
import docking.widgets.table.threaded.ThreadedTableModel;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.FunctionTag;
import ghidra.program.model.listing.Program;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Model that backs a {@link FunctionTagTable}
*/
public class FunctionTagTableModel extends ThreadedTableModel<FunctionTag, List<FunctionTag>> {
/** The list of tags to display in the table */
private List<FunctionTag> tags = new ArrayList<>();
private Program program;
/**
* Constructor
*
* @param modelName the name of this table model
* @param serviceProvider the service provider
*/
protected FunctionTagTableModel(String modelName, ServiceProvider serviceProvider) {
super(modelName, serviceProvider);
}
public void setProgram(Program program) {
this.program = program;
}
@Override
protected void doLoad(Accumulator<FunctionTag> accumulator, TaskMonitor monitor)
throws CancelledException {
accumulator.addAll(tags);
fireTableDataChanged();
}
@Override
protected TableColumnDescriptor<FunctionTag> createTableColumnDescriptor() {
TableColumnDescriptor<FunctionTag> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new FunctionTagNameColumn());
descriptor.addVisibleColumn(new FunctionTagCountColumn());
return descriptor;
}
@Override
public List<FunctionTag> getDataSource() {
return tags;
}
/**
* Adds a function tag to the table. If a tag with the same name is already
* present in the table, does nothing.
*
* @param tag the function tag to add
*/
public void addTag(FunctionTag tag) {
Optional<FunctionTag> existingTag = tags.stream()
.filter(t -> t.getName().equals(tag.getName()))
.findAny();
if (existingTag.isPresent()) {
tags.remove(existingTag.get());
}
tags.add(tag);
fireTableDataChanged();
}
/**
* Removes all function tags from the model
*/
public void clear() {
tags.clear();
fireTableDataChanged();
}
/**
* Returns all function tags in the model
*
* @return all function tags
*/
public List<FunctionTag> getTags() {
return tags;
}
/**
* Returns the {@link FunctionTag} object with a given name
*
* @param name the tag name to search for
* @return the function tag
*/
public FunctionTag getTag(String name) {
Optional<FunctionTag> tag = tags.stream()
.filter(t -> t.getName().equals(name))
.findAny();
if (tag.isPresent()) {
return tag.get();
}
return null;
}
/**
* Returns true if a function tag with a given name is in the model
*
* @param name the tag name to search fo
* @return true if the tag exists in the model
*/
public boolean isTagInModel(String name) {
Optional<FunctionTag> tag = tags.stream()
.filter(t -> t.getName().equals(name))
.findAny();
return tag.isPresent();
}
/**
* Table column that displays a count of the number of times a function tag has been
* applied to a function (in the selected program)
*/
private class FunctionTagCountColumn extends AbstractDynamicTableColumnStub<FunctionTag, Integer> {
@Override
public String getColumnDisplayName(Settings settings) {
return " "; // don't display any name, but need it to be at least one space
// wide so the correct space is allocated to the header
}
@Override
public String getColumnName() {
return "Count";
}
@Override
public int getColumnPreferredWidth() {
return 30;
}
@Override
public Integer getValue(FunctionTag rowObject, Settings settings,
ServiceProvider serviceProvider) throws IllegalArgumentException {
int count = 0;
if (program == null) {
return 0;
}
FunctionIterator iter = program.getFunctionManager().getFunctions(true);
while (iter.hasNext()) {
Function f = iter.next();
Optional<FunctionTag> foundTag = f.getTags()
.stream()
.filter(t -> t.getName().equals(rowObject.getName()))
.findAny();
if (foundTag.isPresent()) {
count++;
}
}
return count;
}
}
/**
* Table column that displays the name of a function tag
*/
private class FunctionTagNameColumn extends AbstractDynamicTableColumnStub<FunctionTag, String> {
@Override
public String getColumnName() {
return "Name";
}
@Override
public String getValue(FunctionTag rowObject, Settings settings,
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getName();
}
}
}

View file

@ -32,8 +32,7 @@ import ghidra.framework.model.DomainObjectChangedEvent;
import ghidra.framework.model.DomainObjectListener;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.*;
import ghidra.program.util.*;
import ghidra.util.*;
@ -60,6 +59,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
private SourceTagsPanel sourcePanel;
private TargetTagsPanel targetPanel;
private FunctionTagButtonPanel buttonPanel;
private AllFunctionsPanel allFunctionsPanel;
private Program program;
private JPanel mainPanel;
@ -69,8 +69,8 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
private HintTextField tagInputTF;
private HintTextField filterInputTF;
private int MIN_WIDTH = 400;
private int MIN_HEIGHT = 150;
private int MIN_WIDTH = 850;
private int MIN_HEIGHT = 350;
// The current program location selected in the listing.
private ProgramLocation currentLocation = null;
@ -79,6 +79,12 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
// the create tag entry field.
private static String INPUT_DELIMITER = ",";
/**
* Constructor
*
* @param plugin the function tag plugin
* @param program the current program
*/
public FunctionTagsComponentProvider(FunctionTagPlugin plugin, Program program) {
super(plugin.getTool(), "Function Tags", plugin.getName(), ProgramActionContext.class);
@ -105,7 +111,6 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
}
updateTitle(currentLocation);
updateTagLists();
});
}
@ -113,7 +118,6 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
@Override
public void componentShown() {
mainPanel = createWorkPanel();
updateTagLists();
updateTitle(currentLocation);
}
@ -124,14 +128,13 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
}
/**
* Invoked when the user has selected a new location in the listing. When
* Invoked when a new location has been detected in the listing. When
* this happens we need to update the tag list to show what tags are assigned
* at the current location.
*
* @param loc the address selected in the listing
*/
public void locationChanged(ProgramLocation loc) {
currentLocation = loc;
updateTitle(loc);
updateTagLists();
@ -159,7 +162,6 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
*/
@Override
public void domainObjectChanged(DomainObjectChangedEvent ev) {
if (ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_CHANGED) ||
ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_CREATED) ||
ev.containsEvent(ChangeManager.DOCR_FUNCTION_TAG_DELETED) ||
@ -183,8 +185,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
setSubTitle("NOT A FUNCTION");
}
else {
setSubTitle(
" " + function.getName() + " " + "(" + function.getEntryPoint().toString() + ")");
setSubTitle("");
}
}
@ -194,53 +195,49 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
// BOTTOM PANEL
JPanel bottomPanel = new JPanel();
bottomPanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 50;
bottomPanel.add(createInputPanel(), gbc);
gbc.gridx = 1;
bottomPanel.add(createFilterPanel(), gbc);
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
bottomPanel.add(createInputPanel());
bottomPanel.add(createFilterPanel());
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
mainPanel.setPreferredSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
// CENTER PANEL
JPanel tagPanel = new JPanel();
tagPanel.setLayout(new GridBagLayout());
sourcePanel = new SourceTagsPanel(this, tool, "Available Tags");
sourcePanel = new SourceTagsPanel(this, tool, "All Tags");
targetPanel = new TargetTagsPanel(this, tool, "Assigned To Function");
allFunctionsPanel = new AllFunctionsPanel(program, this, "Functions with Selected Tag");
buttonPanel = new FunctionTagButtonPanel(sourcePanel, targetPanel);
sourcePanel.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
targetPanel.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
allFunctionsPanel.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0.5;
gbc.weighty = 1.0;
gbc.fill = GridBagConstraints.BOTH;
tagPanel.add(sourcePanel, gbc);
// If we don't set this, then the splitter won't be able to shrink the
// target panels below the size required by its header, which can be large
// because of the amount of text displayed. Keep the minimum size setting on
// the source panel, however. That is generally small.
targetPanel.setMinimumSize(new Dimension(0, 0));
gbc.gridx = 1;
gbc.weightx = 0.0;
tagPanel.add(buttonPanel, gbc);
JPanel wrapper = new JPanel();
wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS));
wrapper.add(sourcePanel);
wrapper.add(buttonPanel);
wrapper.add(targetPanel);
gbc.gridx = 2;
gbc.weightx = 0.5;
tagPanel.add(targetPanel, gbc);
JSplitPane splitter =
new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, wrapper, allFunctionsPanel);
mainPanel.add(tagPanel, BorderLayout.CENTER);
mainPanel.add(splitter, BorderLayout.CENTER);
splitter.setResizeWeight(0.5f);
splitter.setDividerLocation(0.5f);
return mainPanel;
}
/**
* Updates the button panel depending on the selection state of the
* tag lists.
* tag lists. Also updates the {@link AllFunctionsPanel} so it can update
* its list.
*
* @param panel the panel that generated the selection event
*/
@ -256,6 +253,11 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
buttonPanel.targetPanelSelectionChanged(function != null);
sourcePanel.clearSelection();
}
List<FunctionTag> sourceTags = sourcePanel.getSelectedTags();
List<FunctionTag> targetTags = targetPanel.getSelectedTags();
sourceTags.addAll(targetTags);
allFunctionsPanel.setSelectedTags(sourceTags);
}
/**
@ -309,11 +311,23 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
*/
private void updateTagLists() {
if (sourcePanel == null || targetPanel == null) {
if (sourcePanel == null || targetPanel == null || allFunctionsPanel == null) {
return;
}
sourcePanel.setProgram(program);
targetPanel.setProgram(program);
allFunctionsPanel.setProgram(program);
// Get the currently selected tags and use them to update the
// all functions panel. If there is no current selection, leave the
// table as-is.
List<FunctionTag> sTags = sourcePanel.getSelectedTags();
List<FunctionTag> tTags = targetPanel.getSelectedTags();
sTags.addAll(tTags);
if (!sTags.isEmpty()) {
allFunctionsPanel.refresh(sTags);
}
Function function = getFunctionAtLocation(currentLocation);
sourcePanel.refresh(function);
@ -397,7 +411,7 @@ public class FunctionTagsComponentProvider extends ComponentProviderAdapter
}
});
filterPanel.add(new GLabel(" Filter:"), BorderLayout.WEST);
filterPanel.add(new GLabel(" Tag Filter:"), BorderLayout.WEST);
filterPanel.add(filterInputTF, BorderLayout.CENTER);
return filterPanel;

View file

@ -16,7 +16,11 @@
package ghidra.app.plugin.core.function.tags;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import ghidra.app.cmd.function.AddFunctionTagCmd;
import ghidra.app.cmd.function.CreateFunctionTagCmd;
@ -53,20 +57,36 @@ public class SourceTagsPanel extends TagListPanel {
// List of tags read in from the external file. These will be displayed in a different
// color than 'regular' tags and cannot be edited or deleted, until they're added to a function
private List<FunctionTag> tempTags = new ArrayList<>();
// Keeps a list of the original temp tags as loaded from file. This is necessary
// when switching between programs where we need to know the original state of the
// temporary tags. Without this we would need to reload from file on each new program
// activation.
private List<FunctionTag> tempTagsCache;
/**
* Constructor
*
* @param provider the component provider
* @param tool the plugin tool
* @param title the title of the panel
*/
public SourceTagsPanel(FunctionTagsComponentProvider provider, PluginTool tool, String title) {
super(provider, tool, title);
// Load any tags from external sources.
tempTags = loadTags();
// Load any tags from external sources and keep a copy in the cache
tempTags = loadTags();
tempTagsCache = new ArrayList<>(tempTags);
table.setDisabled(true);
}
/******************************************************************************
* PUBLIC METHODS
******************************************************************************/
/**
* Adds any selected to tags to the function currently selected in the
* Adds any selected tags to the function currently selected in the
* listing.
*/
public void addSelectedTags() {
@ -90,74 +110,68 @@ public class SourceTagsPanel extends TagListPanel {
}
@Override
public void refresh(Function f) {
public void refresh(Function function) {
model.clear();
this.function = f;
this.function = function;
try {
tempTags = new ArrayList<>(tempTagsCache);
List<? extends FunctionTag> dbTags = getAllTagsFromDatabase();
for (FunctionTag tag : dbTags) {
model.addElement(tag);
model.addTag(tag);
}
// Now add any temp tags. Note that this is the point at which prune the
// Now add any temp tags. Note that this is the point at which we prune the
// temp tag list to remove any tags that have been added to the database. We
// don't do it when the command for the add has been initiated, we do it here,
// in response to getting the latest items from the db directly.
Iterator<FunctionTag> iter = tempTags.iterator();
while (iter.hasNext()) {
FunctionTag tag = iter.next();
if (dbTags.contains(tag)) {
Optional<? extends FunctionTag> foundTag = dbTags.stream()
.filter(t -> t.getName().equals(tag.getName()))
.findAny();
if (foundTag.isPresent()) {
iter.remove();
}
else {
model.addElement(tag);
model.addTag(tag);
}
}
sortList();
model.reload();
applyFilter();
table.setFunction(function);
}
catch (IOException e) {
Msg.error(this, "Error retrieving tags", e);
}
}
}
/**
* Overridden because after applying the filter we need to then remove
* all assigned items from this list (they will be in the target panel).
* Returns true if all tags in the selection are enabled; false
* otherwise
*
* @return true if all tags in the selection are enabled; false
* otherwise
*/
@Override
protected void applyFilter() {
super.applyFilter();
if (function != null) {
List<FunctionTag> assignedTags = getAssignedTags(function);
for (FunctionTag tag : assignedTags) {
removeTag(tag);
}
public boolean isSelectionEnabled() {
List<FunctionTag> selectedTags = getSelectedTags();
List<FunctionTag> assignedTags = getAssignedTags(function);
if (assignedTags.containsAll(selectedTags)) {
return false;
}
return true;
}
/******************************************************************************
* PRIVATE METHODS
******************************************************************************/
/**
* Removes the given tag from this list.
*
* @param tag the tag to remove
*/
private void removeTag(FunctionTag tag) {
for (int i = 0; i < filteredModel.size(); i++) {
FunctionTag filteredTag = filteredModel.getElementAt(i);
if (filteredTag.getName().equals(tag.getName())) {
filteredModel.removeElement(tag);
}
}
}
/**
* Returns an array of all tags stored in the database.
*

View file

@ -18,49 +18,43 @@ package ghidra.app.plugin.core.function.tags;
import java.awt.BorderLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.swing.DefaultListModel;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import docking.DockingWindowManager;
import docking.widgets.OptionDialog;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.label.GLabel;
import ghidra.app.cmd.function.ChangeFunctionTagCmd;
import ghidra.app.cmd.function.DeleteFunctionTagCmd;
import ghidra.framework.cmd.Command;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.*;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionTag;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
/**
* Generic class for displaying {@link FunctionTag} objects in a list.
* Base panel for displaying tags in the function tag window.
*/
public abstract class TagListPanel extends JPanel {
protected Program program;
// The list object containing the tag names.
protected FunctionTagList list;
// Currently-selected function in the listing.
protected Function function;
// Model representing all tags assigned to the selected function. This is the
// complete, unfiltered set of tags.
protected DefaultListModel<FunctionTag> model = new DefaultListModel<>();
// List of tags to be displayed in the panel. This is the full list with
// filtering applied.
protected DefaultListModel<FunctionTag> filteredModel = new DefaultListModel<>();
protected FunctionTagTable table;
protected PluginTool tool;
protected String filterString = "";
protected FunctionTagTableModel model;
protected FunctionTagTableModel filteredModel;
private JLabel titleLabel;
/**
* Constructor.
* Constructor
*
* @param provider the display provider
* @param tool the plugin tool
@ -70,37 +64,43 @@ public abstract class TagListPanel extends JPanel {
this.tool = tool;
setLayout(new BorderLayout());
model = new FunctionTagTableModel("", provider.getTool()) ;
filteredModel = new FunctionTagTableModel("", provider.getTool());
table = new FunctionTagTable(filteredModel);
table.addMouseListener(new MouseAdapter() {
// Set the model for the list to be the filtered model - we only ever want
// to show the model that has filtering applied.
list = new FunctionTagList(filteredModel);
// When a selection is made in the list, tell the provider so it can update
// the state of buttons, the other list, etc...
list.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
@Override
public void mousePressed(MouseEvent e) {
// Click events aren't reliably captured for some reason,
// but presses are, so this is the best way to ensure that
// user selections are handled
provider.selectionChanged(TagListPanel.this);
}
});
// Mouse listener for handling the double-click event, which will bring up
// a dialog for editing the tag name and/or comment.
list.addMouseListener(new MouseAdapter() {
// Handles the double-click event on table rows, which will bring up
// a dialog for editing the tag name and/or comment.
@Override
public void mouseClicked(MouseEvent evt) {
FunctionTagList list = (FunctionTagList) evt.getSource();
FunctionTagTable table = (FunctionTagTable)evt.getSource();
if (evt.getClickCount() == 2) {
FunctionTag tag = list.getSelectedValue();
int row = table.getSelectedRow();
int nameCol = table.getColumnModel().getColumnIndex("Name");
String tagName = (String) table.getValueAt(row, nameCol);
FunctionTagTableModel model = (FunctionTagTableModel) table.getModel();
FunctionTag tag = model.getTag(tagName);
if (tag == null) {
return;
}
// If the tag is a temporary one, it's not editable. Show a message to the user.
if (tag instanceof FunctionTagTemp) {
Msg.showWarn(list, list, "Tag Not Editable", "Tag " + "\"" + tag.getName() +
Msg.showWarn(this, table, "Tag Not Editable", "Tag " + "\"" + tag.getName() +
"\"" +
" was loaded from an external source and cannot be edited or deleted");
" must be added to the program before it can be modified/deleted");
return;
}
@ -121,7 +121,7 @@ public abstract class TagListPanel extends JPanel {
// If the name is empty, show a warning and don't allow it. A user should
// never want to do this.
if (newName.isEmpty()) {
Msg.showWarn(this, list, "Empty Tag Name?", "Tag name cannot be empty");
Msg.showWarn(this, table, "Empty Tag Name?", "Tag name cannot be empty");
return false;
}
@ -141,8 +141,7 @@ public abstract class TagListPanel extends JPanel {
return true;
});
dialog.setPreferredSize(400, 150);
DockingWindowManager.showDialog(list, dialog);
DockingWindowManager.showDialog(tool.getActiveWindow(), dialog);
if (dialog.isCanceled()) {
return;
@ -150,17 +149,19 @@ public abstract class TagListPanel extends JPanel {
}
}
});
add(new GLabel(title), BorderLayout.NORTH);
add(list, BorderLayout.CENTER);
titleLabel = new JLabel(title);
titleLabel.setBorder(BorderFactory.createEmptyBorder(3, 5, 0, 0));
add(titleLabel, BorderLayout.NORTH);
add(new JScrollPane(table), BorderLayout.CENTER);
}
/******************************************************************************
* PUBLIC METHODS
******************************************************************************/
/**
* Clears the list and repopulates with a new data set. Clients should override this
* Clears the list and repopulates it with a new data set. Clients should override this
* to retrieve data for the given function.
*
* @param function the currently selected function in the listing
@ -168,11 +169,13 @@ public abstract class TagListPanel extends JPanel {
public abstract void refresh(Function function);
public void clearSelection() {
list.clearSelection();
table.clearSelection();
}
public void setProgram(Program program) {
this.program = program;
model.setProgram(program);
filteredModel.setProgram(program);
}
public void setFilterText(String text) {
@ -180,6 +183,10 @@ public abstract class TagListPanel extends JPanel {
applyFilter();
}
public void setTitle(String title) {
titleLabel.setText(title);
}
/**
* Returns true if the tag already exists in the model.
*
@ -187,14 +194,7 @@ public abstract class TagListPanel extends JPanel {
* @return true if the tag exists
*/
public boolean tagExists(String name) {
for (int i = 0; i < model.size(); i++) {
FunctionTag tag = model.getElementAt(i);
if (tag.getName().equals(name)) {
return true;
}
}
return false;
return model.isTagInModel(name);
}
/******************************************************************************
@ -207,27 +207,27 @@ public abstract class TagListPanel extends JPanel {
* @return true if the list has an item selected
*/
protected boolean hasSelection() {
return list.getSelectedIndices().length != 0;
return table.getSelectedRowCount() != 0;
}
/**
* Returns true if at least one of the selected items in the list
* is immutable (a temporary non-user-defined tag that can't be deleted).
* is immutable (a temporary non-user-defined tag that can't be edited/deleted).
*
* @return true if list contains an immutable tag
*/
protected boolean isSelectionImmutable() {
return list.getSelectedValuesList().stream().anyMatch(
val -> val instanceof FunctionTagTemp);
}
protected void sortList() {
List<FunctionTag> myList = Collections.list(model.elements());
Collections.sort(myList);
model.clear();
for (FunctionTag tag : myList) {
model.addElement(tag);
int[] selectedRows = table.getSelectedRows();
int nameCol = table.getColumnModel().getColumnIndex("Name");
for (int i=0; i<selectedRows.length; i++) {
String tagName = (String) table.getValueAt(i, nameCol);
FunctionTag tag = filteredModel.getTag(tagName);
if (tag instanceof FunctionTagTemp) {
return true;
}
}
return false;
}
/**
@ -261,41 +261,51 @@ public abstract class TagListPanel extends JPanel {
}
/**
* Filters the list with the current filter settings.
* Filters the list with the current filter settings
*/
protected void applyFilter() {
filteredModel.clear();
for (int i = 0; i < model.size(); i++) {
if (model.get(i).getName().toLowerCase().contains(filterString.toLowerCase())) {
filteredModel.addElement(model.get(i));
for (FunctionTag tag : model.getTags()) {
if (filterString.isEmpty()) {
filteredModel.addTag(tag);
}
else if (tag.getName().toLowerCase().contains(filterString.toLowerCase())) {
filteredModel.addTag(tag);
}
}
filteredModel.reload();
}
/**
* Retrieves all tags that have been assigned to the given function.
* Retrieves all tags that have been assigned to the given function
*
* @param func the function to get tags for
* @return list of all tags assigned to this function
*/
protected List<FunctionTag> getAssignedTags(Function function) {
protected List<FunctionTag> getAssignedTags(Function func) {
List<FunctionTag> assignedTags = new ArrayList<>();
if (function != null) {
assignedTags.addAll(function.getTags());
if (func != null) {
assignedTags.addAll(func.getTags());
}
return assignedTags;
}
/**
* Returns a list of all tags selected in the list.
* Returns a list of all tags selected in the list
*
* @return the list of function tags
*/
protected List<FunctionTag> getSelectedTags() {
List<FunctionTag> tags = new ArrayList<>();
int[] selectedIndices = list.getSelectedIndices();
int[] selectedIndices = table.getSelectedRows();
for (int i : selectedIndices) {
tags.add(filteredModel.getElementAt(i));
String tagName = (String) filteredModel.getValueAt(i, 0);
Optional<FunctionTag> tag = filteredModel.getTags().stream().filter(t -> t.getName().equals(tagName)).findAny();
if (tag.isPresent()) {
tags.add(tag.get());
}
}
return tags;

View file

@ -26,14 +26,22 @@ import ghidra.program.model.listing.FunctionTag;
/**
* Displays a list of tags that have been assigned to the currently-selected
* function in the listing. If no function is selected, the list will be
* empty.
* function in the listing
*/
public class TargetTagsPanel extends TagListPanel {
/**
* Constructor
*
* @param provider the component provider
* @param tool the plugin tool
* @param title the panel title
*/
public TargetTagsPanel(FunctionTagsComponentProvider provider,
PluginTool tool, String title) {
super(provider, tool, title);
table.setDisabled(false);
}
/******************************************************************************
@ -42,18 +50,27 @@ public class TargetTagsPanel extends TagListPanel {
@Override
public void refresh(Function function) {
model.clear();
this.function = function;
if (function == null) {
setTitle("No Function Selected");
}
else {
setTitle(function.getName() + " " + "(" + function.getEntryPoint().toString() + ")");
}
List<FunctionTag> assignedTags = getAssignedTags(function);
Collections.sort(assignedTags);
for (FunctionTag tag : assignedTags) {
model.addElement(tag);
model.addTag(tag);
}
this.function = function;
sortList();
model.reload();
applyFilter();
table.setFunction(function);
}
/**

View file

@ -15,15 +15,23 @@
*/
package ghidra.app.plugin.core.function.tags;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.awt.Dimension;
import java.io.IOException;
import java.util.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.swing.AbstractButton;
import org.junit.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import docking.ActionContext;
import docking.action.DockingActionIf;
@ -37,10 +45,13 @@ import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.function.FunctionDB;
import ghidra.program.database.function.FunctionManagerDB;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionTag;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.test.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.ClassicSampleX86ProgramBuilder;
import ghidra.test.TestEnv;
import ghidra.util.exception.UsrException;
/**
@ -63,12 +74,10 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
// dialog.
private DockingActionIf editFunctionTags;
private static final int DIALOG_WAIT_TIME = 3000;
// Define addresses for the first two functions in the test program;
// these will handle most cases we need to test.
private Address NON_FUNCTION_ADDRESS;
private Address FUNCTION_ENTRY_ADDRESS;
private Address FUNCTION_ENTRY_ADDRESS_2;
private Address FUNCTION_ENTRY_ADDRESS_3;
// The UI we're testing.
private FunctionTagsComponentProvider provider = null;
@ -99,6 +108,8 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
NON_FUNCTION_ADDRESS = addr("010022b8");
FUNCTION_ENTRY_ADDRESS = addr("01002239");
FUNCTION_ENTRY_ADDRESS_2 = addr("0100248f");
FUNCTION_ENTRY_ADDRESS_3 = addr("0100299e");
goToFunction(FUNCTION_ENTRY_ADDRESS);
showDialog();
@ -144,20 +155,18 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
* non-immutable tags to the selection</li>
* <li>Verify that the delete button will become enabled if we
* remove any immutable tags from the selection</li>
*
* @throws UsrException if there's an error retrieving tag panel components
* @throws IOException if there's an error retrieving tags from the database
* @throws Exception if there is a problem selecting tags
*/
@Test
public void testDeleteImmutableTag() throws IOException, UsrException {
public void testDeleteImmutableTag() throws Exception {
FunctionTagList list = getTagListInPanel("sourcePanel");
FunctionTagTable table = getTagListInPanel("sourcePanel");
// Get an immutable tag from the source panel and set it to be selected. Verify that
// the delete button is disabled.
FunctionTagTemp immutableTag = getImmutableTag();
assertTrue("Must have at least one immutable tag for this test", immutableTag != null);
selectTagInList(immutableTag.getName(), list);
selectTagInList(immutableTag.getName(), table);
waitForSwing();
assertFalse(isButtonEnabled("deleteBtn"));
@ -166,13 +175,13 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
String tagName = "TAG 1";
createTag(tagName);
waitForSwing();
list.setSelectionInterval(0, list.getModel().getSize() - 1);
table.addRowSelectionInterval(0, table.getRowCount()-1);
waitForSwing();
assertFalse(isButtonEnabled("deleteBtn"));
// Select just the non-immutable tag and verify that the delete button is now
// enabled.
selectTagInList(tagName, list);
selectTagInList(tagName, table);
waitForSwing();
assertTrue(isButtonEnabled("deleteBtn"));
}
@ -180,14 +189,12 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
/**
* Verifies that we can delete a previously immutable tag once it has been assigned
* to a function.
*
* @throws UsrException if there's an error retrieving tag panel components
* @throws IOException if there's an error retrieving tags from the database
* @throws Exception if there is a problem adding tags to functions
*/
@Test
public void testDeleteImmutableTagAfterUse() throws UsrException, IOException {
public void testDeleteImmutableTagAfterUse() throws Exception {
FunctionTagList list = getTagListInPanel("targetPanel");
FunctionTagTable table = getTagListInPanel("targetPanel");
// Get an immutable tag from the source panel.
FunctionTagTemp tag = getImmutableTag();
@ -201,7 +208,7 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
boolean inList = isTagNameInList(tag.getName(), getAllTags());
assertTrue(inList);
selectTagInList(tag.getName(), list);
selectTagInList(tag.getName(), table);
waitForSwing();
assertTrue(isButtonEnabled("deleteBtn"));
}
@ -282,11 +289,11 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
/**
* Tests that we can add a tag to a function.
*
* @throws UsrException if there's an error adding the tag from the function
*
* @throws Exception if there is a problem adding tags to functions
*/
@Test
public void testAddTagToFunction() throws UsrException {
public void testAddTagToFunction() throws Exception {
String name = "TEST";
createTag(name);
addTagToFunction(name, FUNCTION_ENTRY_ADDRESS);
@ -295,11 +302,10 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
/**
* Tests that we can remove a tag from a function.
*
* @throws UsrException if there's an error adding or removing the tag from the function
* @throws Exception if there is a problem adding/removing tags to functions
*/
@Test
public void testRemoveTagFromFunction() throws UsrException {
public void testRemoveTagFromFunction() throws Exception {
String name = "TEST";
createTag(name);
addTagToFunction(name, FUNCTION_ENTRY_ADDRESS);
@ -307,6 +313,120 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
assertTrue(!isTagNameInList(name, getTagsForFunctionAt(FUNCTION_ENTRY_ADDRESS)));
}
/**
* Verifies that the tags assigned to a function are visible in the function tag
* panel
* @throws Exception if there is a problem adding tags to functions
*/
@Test
public void testViewFunctionsForTag() throws Exception {
// Verify that the function panel is initially empty
AllFunctionsPanel functionsPanel = getFunctionsPanel();
List<Function> functions = functionsPanel.getFunctions();
assert (functions.isEmpty());
// Create a new tag and add it to a function
String tagName1 = "TAG 1";
createTag(tagName1);
addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS);
// Select the tag in the target panel
FunctionTagTable table = getTagListInPanel("targetPanel");
selectTagInList(tagName1, table);
waitForTableModel(functionsPanel.getTableModel());
// Verify that the function is shown in the function panel (check that
// we have exactly 1 match, and that the address is for the correct
// function)
functions = functionsPanel.getFunctions();
assert (functions.size() == 1);
Function f = functions.get(0);
assert (f.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS));
}
/**
* Verifies if a tag is assigned to multiple functions they will all be shown
* in the function panel
*
* @throws Exception if there's a problem adding tags to functions
*/
@Test
public void testMultipleFunctionsWithTag() throws Exception {
// Verify that the function panel is initially empty
AllFunctionsPanel functionsPanel = getFunctionsPanel();
List<Function> functions = functionsPanel.getFunctions();
assert (functions.isEmpty());
// Create a new tag and add it to both functions
String tagName1 = "TAG 1";
createTag(tagName1);
addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS);
addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS_2);
// Select the tag in the target panel
FunctionTagTable table = getTagListInPanel("targetPanel");
selectTagInList(tagName1, table);
waitForTableModel(functionsPanel.getTableModel());
// Verify that both functions are shown in the function panel (check that
// we have exactly 2 matches, and that the addresses are for the correct
// functions)
functions = functionsPanel.getFunctions();
assert (functions.size() == 2);
Function f1 = functions.get(0);
Function f2 = functions.get(1);
assert (f1.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS));
assert (f2.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS_2));
}
/**
* Verifies if multiple tags are selected, all functions containing that tag
* are displayed
*
* @throws Exception if there's a problem adding tags to functions
*/
@Test
public void testViewMultipleFunctions() throws Exception {
// Verify that the function panel is initially empty
AllFunctionsPanel functionsPanel = getFunctionsPanel();
List<Function> functions = functionsPanel.getFunctions();
assert (functions.isEmpty());
// Create two new tags and add them to the functions
String tagName1 = "TAG 1";
createTag(tagName1);
addTagToFunction(tagName1, FUNCTION_ENTRY_ADDRESS);
String tagName2 = "TAG 2";
createTag(tagName2);
addTagToFunction(tagName2, FUNCTION_ENTRY_ADDRESS_2);
addTagToFunction(tagName2, FUNCTION_ENTRY_ADDRESS_3);
// Select both tags and verify that 3 functions are listed in
// the functions panel
goTo(tool, program, addr("00000000"));
FunctionTagTable table = getTagListInPanel("sourcePanel");
selectTagInList(tagName1, table);
int index = table.getSelectedRow();
table.addRowSelectionInterval(index, index+1);
clickTableRange(table, index, 2);
waitForTableModel(functionsPanel.getTableModel());
// Verify that all 3 functions are in the function panel
functions = functionsPanel.getFunctions();
assert (functions.size() == 3);
Function f1 = functions.get(0);
Function f2 = functions.get(1);
Function f3 = functions.get(2);
assert (f1.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS));
assert (f2.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS_2));
assert (f3.getEntryPoint().equals(FUNCTION_ENTRY_ADDRESS_3));
}
/****************************************************************************************
* PRIVATE METHODS
****************************************************************************************/
@ -333,14 +453,14 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
*
* @param name the tag name to add
* @param address the function entry point
* @throws UsrException if there's an error retrieving the source panel instance
* @throws Exception if there is a problem selecting tags or clicking buttons
*/
private void addTagToFunction(String name, Address address) throws UsrException {
private void addTagToFunction(String name, Address address) throws Exception {
cb.goTo(new ProgramLocation(program, address));
waitForSwing();
FunctionTagList list = getTagListInPanel("sourcePanel");
FunctionTagTable list = getTagListInPanel("sourcePanel");
selectTagInList(name, list);
clickButtonByName("addBtn");
}
@ -350,54 +470,74 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
*
* @param name the tag name to remove
* @param address the function entry point
* @throws UsrException if there's an error retrieving the target panel instance
* @throws Exception if there is a problem selecting tags or clicking buttons
*/
private void removeTagFromFunction(String name, Address address) throws UsrException {
private void removeTagFromFunction(String name, Address address) throws Exception {
cb.goTo(new ProgramLocation(program, address));
waitForSwing();
FunctionTagList list = getTagListInPanel("targetPanel");
selectTagInList(name, list);
FunctionTagTable table = getTagListInPanel("targetPanel");
selectTagInList(name, table);
clickButtonByName("removeBtn");
}
/**
* Gets the instance of the tag list in the given panel.
* Gets the instance of the tag table in the given panel.
*
* @param panelName the name of the panel to search
* @return the function tag list
* @throws UsrException if there's an error retrieving the panel or tag list instances
*/
private FunctionTagList getTagListInPanel(String panelName) throws UsrException {
private FunctionTagTable getTagListInPanel(String panelName) throws UsrException {
Object comp = getInstanceField(panelName, provider);
if (comp == null) {
throw new UsrException("Error getting targetPanel field in provider");
}
TagListPanel panel = (TagListPanel) comp;
Object list = getInstanceField("list", panel);
if (list == null) {
throw new UsrException("Error getting list field in TargetTagsPanel");
Object table = getInstanceField("table", comp);
if (table == null) {
throw new UsrException("Error getting table in tag panel");
}
FunctionTagList tagList = (FunctionTagList) list;
return tagList;
FunctionTagTable tagTable = (FunctionTagTable) table;
return tagTable;
}
/**
* Selects the item in the list with the given name.
* Returns the functions panel
*
* @return the functions panel
*/
private AllFunctionsPanel getFunctionsPanel() {
AllFunctionsPanel panel =
(AllFunctionsPanel) getInstanceField("allFunctionsPanel", provider);
return panel;
}
/**
* Selects the item in the table with the given name.
*
* @param name the tag name to select
* @param list the list to search
* @throws UsrException if there's an error retrieving tag from the list
* @param table the table to search
* @throws Exception if there is a problem clicking cells in a list
*/
private void selectTagInList(String name, FunctionTagList list) throws UsrException {
FunctionTag tag = getListItemByName(name, list);
private void selectTagInList(String name, FunctionTagTable table) throws Exception {
FunctionTag tag = getListItemByName(name, table);
if (tag == null) {
throw new UsrException("Error retrieving tag with name: " + name);
}
list.setSelectedValue(tag, true);
int row = 0;
for (int i=0; i<table.getRowCount(); i++) {
String tagname = (String) table.getValueAt(i, 0);
if (tagname.equals(name)) {
table.addRowSelectionInterval(i,i);
row = i;
}
}
clickTableCell(table, row, 0, 1);
waitForSwing();
}
@ -438,12 +578,13 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
* Returns the list item (FunctionTag) that has the given tag name.
*
* @param name the tag name
* @param list the list to search
* @param table the table to search
*/
private FunctionTag getListItemByName(String name, FunctionTagList list) {
int count = list.getModel().getSize();
private FunctionTag getListItemByName(String name, FunctionTagTable table) {
int count = table.getRowCount();
for (int i = 0; i < count; i++) {
FunctionTag tag = list.getModel().getElementAt(i);
FunctionTagTableModel model = (FunctionTagTableModel)table.getModel();
FunctionTag tag = model.getTag(name);
if (tag.getName().equals(name)) {
return tag;
}
@ -467,14 +608,10 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
/**
* Displays the function tag dialog.
*
* @return the dialog component provider
*/
private void showDialog() {
performAction(editFunctionTags, cb.getProvider(), false);
provider = waitForComponentProvider(tool.getToolFrame(),
FunctionTagsComponentProvider.class, DIALOG_WAIT_TIME);
provider = waitForComponentProvider(FunctionTagsComponentProvider.class);
tool.showComponentProvider(provider, true);
}
@ -549,13 +686,11 @@ public class FunctionTagEditorTest extends AbstractGhidraHeadedIntegrationTest {
*/
private FunctionTagTemp getImmutableTag() throws UsrException {
FunctionTagList list = getTagListInPanel("sourcePanel");
for (int i = 0; i < list.getModel().getSize(); i++) {
FunctionTag tag = list.getModel().getElementAt(i);
if (tag instanceof FunctionTagTemp) {
return (FunctionTagTemp) tag;
}
FunctionTagTable table = getTagListInPanel("sourcePanel");
FunctionTagTableModel model = (FunctionTagTableModel)table.getModel();
Optional<FunctionTag> foundTag = model.getTags().stream().filter(t -> t instanceof FunctionTagTemp).findAny();
if (foundTag.isPresent()) {
return (FunctionTagTemp)foundTag.get();
}
return null;

View file

@ -15,7 +15,7 @@
*/
package generic.test;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
import java.awt.*;
import java.awt.event.*;
@ -1296,6 +1296,46 @@ public abstract class AbstractGenericTest extends AbstractGTest {
waitForSwing();
}
/**
* Clicks a range of items in a list (simulates holding SHIFT and selecting
* each item in the range in-turn)
*
* @param list the list to select from
* @param row the initial index
* @param count the number of rows to select
* @throws Exception if there's a problem simulating the click
*/
public static void clickListRange(final JList list, final int row, int count)
throws Exception {
waitForSwing();
for (int i = row; i < row + count; i++) {
Rectangle rect = list.getCellBounds(i, i);
clickMouse(list, MouseEvent.BUTTON1, rect.x + 10, rect.y + 10, 1,
InputEvent.SHIFT_DOWN_MASK);
}
waitForSwing();
}
/**
* Clicks a range of items in a table (simulates holding SHIFT and selecting
* each item in the range)
*
* @param table the table to select
* @param row the starting row index
* @param count the number of rows to select
* @throws Exception
*/
public static void clickTableRange(final JTable table, final int row, int count)
throws Exception {
waitForSwing();
for (int i = row; i < row + count; i++) {
Rectangle rect = table.getCellRect(i, 0, true);
clickMouse(table, MouseEvent.BUTTON1, rect.x + 10, rect.y + 10, 1,
InputEvent.SHIFT_DOWN_MASK);
}
waitForSwing();
}
public static TableCellEditor editCell(final JTable table, final int row, final int col) {
waitForSwing();

View file

@ -19,15 +19,21 @@ import static org.junit.Assert.assertTrue;
import java.awt.Rectangle;
import javax.swing.*;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.junit.Test;
import docking.widgets.OptionDialog;
import docking.widgets.dialogs.InputDialog;
import ghidra.app.plugin.core.function.tags.*;
import ghidra.app.plugin.core.function.tags.FunctionTagButtonPanel;
import ghidra.app.plugin.core.function.tags.FunctionTagTableModel;
import ghidra.app.plugin.core.function.tags.FunctionTagTable;
import ghidra.app.plugin.core.function.tags.FunctionTagsComponentProvider;
import ghidra.app.plugin.core.function.tags.SourceTagsPanel;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.util.ProgramLocation;
import ghidra.util.exception.UsrException;
@ -38,7 +44,7 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
showProvider(FunctionTagsComponentProvider.class);
waitForSwing();
addTableData();
captureIsolatedProvider(FunctionTagsComponentProvider.class, 400, 300);
captureIsolatedProvider(FunctionTagsComponentProvider.class, 950, 400);
}
@Test
@ -71,9 +77,9 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class);
SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider);
FunctionTagList list = (FunctionTagList) getInstanceField("list", sourcePanel);
Rectangle bounds = list.getCellBounds(7, 7); // Cell 7 is an editable item
doubleClick(list, bounds.x, bounds.y);
FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel);
Rectangle bounds = table.getCellRect(7, 0, false); // Cell 7 is an editable item
doubleClick(table, bounds.x, bounds.y);
InputDialog warningDialog = waitForDialogComponent(InputDialog.class);
@ -95,8 +101,8 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class);
SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider);
FunctionTagList list = (FunctionTagList) getInstanceField("list", sourcePanel);
list.setSelectedIndex(7);
FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel);
table.setRowSelectionInterval(7, 7);
FunctionTagButtonPanel buttonPanel =
(FunctionTagButtonPanel) getInstanceField("buttonPanel", provider);
pressButtonByName(buttonPanel, "deleteBtn", false);
@ -117,8 +123,8 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
FunctionTagsComponentProvider provider = getProvider(FunctionTagsComponentProvider.class);
SourceTagsPanel sourcePanel = (SourceTagsPanel) getInstanceField("sourcePanel", provider);
FunctionTagList list = (FunctionTagList) getInstanceField("list", sourcePanel);
doubleClickItem(list, "LIBRARY"); // pick a known read-only tag
FunctionTagTable table = (FunctionTagTable) getInstanceField("table", sourcePanel);
doubleClickItem(table, "LIBRARY"); // pick a known read-only tag
OptionDialog warningDialog = waitForDialogComponent(OptionDialog.class);
captureDialog(warningDialog);
@ -128,15 +134,13 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
// Private Methods
//==================================================================================================
private void doubleClickItem(FunctionTagList list, String text) {
private void doubleClickItem(FunctionTagTable table, String text) {
ListModel<FunctionTag> model = list.getModel();
int n = model.getSize();
FunctionTagTableModel model = (FunctionTagTableModel) table.getModel();
int row = -1;
for (int i = 0; i < n; i++) {
FunctionTag tag = model.getElementAt(i);
String name = tag.getName();
if (text.equals(name)) {
for (int i=0; i<model.getRowCount(); i++) {
String name = (String) table.getValueAt(i, 0);
if (name.equals(text)) {
row = i;
break;
}
@ -144,8 +148,8 @@ public class FunctionTagPluginScreenShots extends GhidraScreenShotGenerator {
assertTrue("Could not find tag '" + text + "'", row > -1);
Rectangle bounds = list.getCellBounds(row, row);
doubleClick(list, bounds.x, bounds.y);
Rectangle bounds = table.getCellRect(row, 0, true);
doubleClick(table, bounds.x, bounds.y);
}
private void addTableData() {