mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-09-20 09:31:47 +00:00
GP-4410 - Version Tracking - Added support for deleting matches; Added table column filters
This commit is contained in:
parent
76977bd514
commit
9f73d23ee4
|
@ -26,6 +26,7 @@ project.ext.excludeFromParallelIntegrationTests = true
|
|||
|
||||
dependencies {
|
||||
api project(":Base")
|
||||
runtimeOnly project(":CodeCompare")
|
||||
|
||||
testImplementation project(path: ':Project', configuration: 'testArtifacts')
|
||||
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
|
||||
|
|
|
@ -315,9 +315,56 @@
|
|||
|
||||
<P align="left"><A name="Clear_Match"></A>The <b>Clear Match</b> <IMG src="images/undo-apply.png" border="0">
|
||||
action will reset the match to unaccepted and undo any applied markup.</P>
|
||||
|
||||
|
||||
|
||||
<P align="left"><A name="Remove_Match"></A>The <b>Remove Match</b> <IMG src="images/edit-delete.png" border="0">
|
||||
action will remove a manually created match from the matches table.</P>
|
||||
action will remove the selected match(es).</P>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
<P><IMG src="help/shared/warning.png" alt="Note" border="0">
|
||||
|
||||
As of Ghidra 11.2, Version Tracking supports deleting matches. Any match that has
|
||||
not been accepted can be deleted without confirmation. However, if you attempt to
|
||||
delete an <B>accepted match</B> that is the <B>last match for an association</B>, then
|
||||
you will be prompted to confirm your decision.
|
||||
</P>
|
||||
<P>
|
||||
Generally, we suggest users should not delete accepted matches. The more matches that
|
||||
are accepted, the better the Version Tracking results, since user choices affect
|
||||
future match scores. Keeping accepted matches and the applied markup provides
|
||||
future analysis with more corroborating details. Contrastingly, deleting accepted
|
||||
matches while keeping applied markup will remove supporting evidence that the user has
|
||||
already substantiated.
|
||||
</P>
|
||||
<BLOCKQUOTE>
|
||||
<P><IMG src="help/shared/tip.png">An alternative to deleting matches is to
|
||||
simply filter them out of the table once they have been applied. You can also tag
|
||||
any matches you wish to ignore and then use the <A HREF="#VT_Advanced_Filters">
|
||||
advanced filters</a> to hide any matches with those tags.
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
<P>
|
||||
It is important to understand what happens in Version Tracking when deleting a match.
|
||||
You will have to make a decision before deleting whether you want to keep any changes
|
||||
made to the destination program when you accepted a given match or whether you wish to
|
||||
remove that markup. When deleting an accepted match:
|
||||
</P>
|
||||
<BLOCKQUOTE>
|
||||
<P>
|
||||
<U>To keep all applied markup</U>, simply delete the match and, when
|
||||
prompted, choose <B>Delete Accepted Matches</B>. This choice will delete the match and
|
||||
its markup items, but <B>any applied markup item content will remain in the destination
|
||||
program.</B> Alternatively, when prompted, you can choose <B>Finish</B> which will
|
||||
close the prompt dialog and will not delete the remaining accepted matches or markup.
|
||||
<P>
|
||||
<U>To remove all applied markup</U>, then you must first
|
||||
<A HREF="#Clear_Match">clear</A> the match before executing the remove action. The clear
|
||||
action will remove applied markup. After clearing the match, then you can remove
|
||||
the match and no markup will remain in the destination program.
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
</BLOCKQUOTE>
|
||||
|
||||
<P align="left"><A name="Make_Selections"></A>The <b>Make Selections</b> <IMG src="Icons.MAKE_SELECTION_ICON" border="0">
|
||||
action will create selections in the source and destination tools for all matches selected in the table.</P>
|
||||
|
@ -400,79 +447,101 @@
|
|||
|
||||
<H2><A name="Match_Filters"></A>Match Filters</H2>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
<P align="left">The match table has an extensive assortment of filters. There
|
||||
are several commonly used filter controls at the bottom of the table:
|
||||
<ol>
|
||||
<li><b>Text Filter</b> - allows you to filter based on any text in the table
|
||||
</li>
|
||||
<li><b>Score Filter</b> - allows you to filter on a range of scores. All scores
|
||||
are between 0 and 1
|
||||
</li>
|
||||
<li><b>Confidence Filter</b> - allows you to filter a range of confidence values.
|
||||
All confidence values will be greater than -9.999 and smaller than 9.999.
|
||||
</li>
|
||||
<li><b>Length Filter</b> - is used to filter out
|
||||
functions that are smaller than some number
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</P>
|
||||
|
||||
<P>Finally, the <IMG src="images/view-filter.png" border="0"> will show the ancillary filters
|
||||
available. The table below lists and describes the available filters. When an ancillary
|
||||
filter is applied, the icon will change to <IMG src="images/lightbulb.png" border="0"> .
|
||||
Further, the icon may occasionally flash as a reminder that there is a filter applied.</P>
|
||||
<BR>
|
||||
<TABLE border="1" width="90%">
|
||||
<TR>
|
||||
<TH nowrap>Filter Name</TH>
|
||||
|
||||
<TH>Description</TH>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top">Match Type</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only function or data matches.</TD>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top" nowrap>Association Status</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose assocation
|
||||
has one of the included status types. A useful setting
|
||||
for this filter is to turn off all but the <B>Available</B> status. This will cause the
|
||||
table to act like a "To Do" list.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Symbol Type</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose source or
|
||||
destination labels are of one of the included symbol types.</TD>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top">Algorithms</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches that were generated
|
||||
by one of the included types of correlating algorithms</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Address Range</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose source or
|
||||
destination address is within the specified range.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Tags</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose tag is an
|
||||
included tag.</TD>
|
||||
</TR>
|
||||
<BLOCKQUOTE>
|
||||
|
||||
<H3>Table Filters</H3>
|
||||
|
||||
|
||||
</TABLE>
|
||||
<BLOCKQUOTE>
|
||||
<P align="left">The match table has an extensive assortment of filters. There
|
||||
are several commonly used filter controls at the bottom of the table:
|
||||
<ol>
|
||||
<li><b>Text Filter</b> - allows you to filter based on any text in the table
|
||||
</li>
|
||||
<li><b>Score Filter</b> - allows you to filter on a range of scores. All scores
|
||||
are between 0 and 1
|
||||
</li>
|
||||
<li><b>Confidence Filter</b> - allows you to filter a range of confidence values.
|
||||
All confidence values will be greater than -9.999 and smaller than 9.999.
|
||||
</li>
|
||||
<li><b>Length Filter</b> - is used to filter out
|
||||
functions that are smaller than some number
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
|
||||
<A NAME="VT_Advanced_Filters"></A>
|
||||
<H3>Advanced Filters</H3>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
|
||||
<P>Finally, the <IMG src="icon.version.tracking.unfiltered" border="0"> will show the ancillary filters
|
||||
available. The table below lists and describes the available filters. When an ancillary
|
||||
filter is applied, the icon will change to <IMG src="icon.version.tracking.filtered" border="0"> .
|
||||
Further, the icon may occasionally flash as a reminder that there is a filter applied.</P>
|
||||
<BR>
|
||||
<TABLE border="1" width="90%">
|
||||
<TR>
|
||||
<TH nowrap>Filter Name</TH>
|
||||
|
||||
<TH>Description</TH>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top">Match Type</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only function or data matches.</TD>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top" nowrap>Association Status</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose assocation
|
||||
has one of the included status types. A useful setting
|
||||
for this filter is to turn off all but the <B>Available</B> status. This will cause the
|
||||
table to act like a "To Do" list.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Symbol Type</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose source or
|
||||
destination labels are of one of the included symbol types.</TD>
|
||||
</TR>
|
||||
|
||||
<TR>
|
||||
<TD valign="top">Algorithms</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches that were generated
|
||||
by one of the included types of correlating algorithms</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Address Range</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose source or
|
||||
destination address is within the specified range.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD valign="top">Tags</TD>
|
||||
|
||||
<TD valign="top">This filter allows the user to show only matches whose tag is an
|
||||
included tag.</TD>
|
||||
</TR>
|
||||
|
||||
|
||||
</TABLE>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
<H3>Table Column Filters</H3>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
The matches table also supports
|
||||
<A HREF="help/topics/Trees/GhidraTreeFilter.html#Column_Filters">
|
||||
Table Column Filters</A> for creating complex filters for individual table columns.
|
||||
</BLOCKQUOTE>
|
||||
|
||||
</BLOCKQUOTE>
|
||||
|
||||
</BLOCKQUOTE> <!-- end of top-level blockquote -->
|
||||
|
|
|
@ -21,8 +21,7 @@ import java.io.IOException;
|
|||
import java.util.*;
|
||||
|
||||
import db.*;
|
||||
import ghidra.feature.vt.api.impl.MarkupItemStorage;
|
||||
import ghidra.feature.vt.api.impl.VTEvent;
|
||||
import ghidra.feature.vt.api.impl.*;
|
||||
import ghidra.feature.vt.api.main.*;
|
||||
import ghidra.feature.vt.api.util.VTAssociationStatusException;
|
||||
import ghidra.framework.data.OpenMode;
|
||||
|
@ -138,33 +137,13 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
|||
return createMarkupItemDB(markupItemStorage);
|
||||
}
|
||||
|
||||
void removeMarkupItem(MarkupItemStorageDB appliedMarkupItemDB) {
|
||||
// non-interface method; internal API use
|
||||
public void removeStoredMarkupItems(List<MarkupItemImpl> impls) {
|
||||
|
||||
VTAssociationDB association = (VTAssociationDB) appliedMarkupItemDB.getAssociation();
|
||||
|
||||
validateAcceptedState(appliedMarkupItemDB, association);
|
||||
|
||||
try {
|
||||
markupItemTableAdapter.removeMatchMarkupItemRecord(appliedMarkupItemDB.getKey());
|
||||
}
|
||||
catch (IOException e) {
|
||||
session.dbError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAcceptedState(MarkupItemStorageDB appliedItem,
|
||||
VTAssociationDB association) {
|
||||
//
|
||||
// For any 'applied' markup item we assume that its association will be 'ACCEPTED'. The
|
||||
// exception to this rule is when we have markup items in the database, but that are not
|
||||
// applied (like when we change the destination address without applying)
|
||||
//
|
||||
VTAssociationStatus associationStatus = association.getStatus();
|
||||
VTMarkupItemStatus status = appliedItem.getStatus();
|
||||
if (status.isUnappliable()) {
|
||||
if (associationStatus != ACCEPTED) {
|
||||
throw new AssertException("Cannot have an applied markup item with an " +
|
||||
"association that is not ACCEPTED");
|
||||
for (MarkupItemImpl impl : impls) {
|
||||
MarkupItemStorage storage = impl.getStorage();
|
||||
if (storage instanceof MarkupItemStorageDB storageDb) {
|
||||
removeMarkupRecord(storageDb.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -251,17 +230,31 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
|||
}
|
||||
|
||||
void removeAssociation(VTAssociation association) {
|
||||
VTAssociationDB existingAssociation = (VTAssociationDB) association;
|
||||
long id = existingAssociation.getKey();
|
||||
|
||||
// Update the association status so that we update any blocked associations
|
||||
VTAssociationDB associationDB = (VTAssociationDB) association;
|
||||
VTAssociationStatus status = association.getStatus();
|
||||
if (status == ACCEPTED) {
|
||||
associationDB.setStatus(AVAILABLE);
|
||||
associationDB.setInvalid();
|
||||
unblockRelatedAssociations(associationDB);
|
||||
for (AssociationHook hook : associationHooks) {
|
||||
hook.associationCleared(associationDB);
|
||||
}
|
||||
}
|
||||
|
||||
VTAssociationDB associationDb = (VTAssociationDB) association;
|
||||
long id = associationDb.getKey();
|
||||
try {
|
||||
associationDb.removeMarkupItems();
|
||||
associationTableAdapter.removeAssociaiton(id);
|
||||
session.setChanged(VTEvent.ASSOCIATION_REMOVED, existingAssociation, null);
|
||||
session.setChanged(VTEvent.ASSOCIATION_REMOVED, associationDb, null);
|
||||
}
|
||||
catch (IOException e) {
|
||||
session.dbError(e);
|
||||
}
|
||||
associationCache.delete(id);
|
||||
existingAssociation.setInvalid();
|
||||
associationDb.setInvalid();
|
||||
|
||||
}
|
||||
|
||||
|
@ -507,7 +500,7 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
|||
throws VTAssociationStatusException {
|
||||
if (association.hasAppliedMarkupItems()) {
|
||||
throw new VTAssociationStatusException(
|
||||
"VTMarkupItemManager contains applied " + "markup items");
|
||||
"VTMarkupItemManager contains applied markup items");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -623,9 +616,9 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
|||
associationHooks.remove(hook);
|
||||
}
|
||||
|
||||
void removeMarkupRecord(DBRecord record) {
|
||||
void removeMarkupRecord(long key) {
|
||||
try {
|
||||
markupItemTableAdapter.removeMatchMarkupItemRecord(record.getKey());
|
||||
markupItemTableAdapter.removeMarkupItemRecord(key);
|
||||
}
|
||||
catch (IOException e) {
|
||||
session.dbError(e);
|
||||
|
|
|
@ -135,7 +135,7 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto
|
|||
try {
|
||||
MarkupItemStorage storage = new MarkupItemStorageImpl(getAssociation(), getMarkupType(),
|
||||
getSourceAddress(), getDestinationAddress(), getDestinationAddressSource());
|
||||
associationManager.removeMarkupRecord(record);
|
||||
associationManager.removeMarkupRecord(record.getKey());
|
||||
return storage;
|
||||
}
|
||||
finally {
|
||||
|
@ -144,7 +144,8 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto
|
|||
}
|
||||
|
||||
@Override
|
||||
public MarkupItemStorage setDestinationAddress(Address destinationAddress, String addressSource) {
|
||||
public MarkupItemStorage setDestinationAddress(Address destinationAddress,
|
||||
String addressSource) {
|
||||
if (destinationAddress == null) {
|
||||
destinationAddress = Address.NO_ADDRESS;
|
||||
}
|
||||
|
|
|
@ -296,4 +296,8 @@ public class VTAssociationDB extends DatabaseObject implements VTAssociation {
|
|||
public boolean hasAppliedMarkupItems() {
|
||||
return markupManager.hasAppliedMarkupItems();
|
||||
}
|
||||
|
||||
void removeMarkupItems() {
|
||||
markupManager.removeMarkupItems();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ public abstract class VTMatchMarkupItemTableDBAdapter {
|
|||
|
||||
public abstract RecordIterator getRecords() throws IOException;
|
||||
|
||||
public abstract void removeMatchMarkupItemRecord(long key) throws IOException;
|
||||
public abstract void removeMarkupItemRecord(long key) throws IOException;
|
||||
|
||||
public abstract DBRecord getRecord(long key) throws IOException;
|
||||
|
||||
|
@ -70,5 +70,6 @@ public abstract class VTMatchMarkupItemTableDBAdapter {
|
|||
|
||||
public abstract int getRecordCount();
|
||||
|
||||
public abstract DBRecord createMarkupItemRecord(MarkupItemStorage markupItem) throws IOException;
|
||||
public abstract DBRecord createMarkupItemRecord(MarkupItemStorage markupItem)
|
||||
throws IOException;
|
||||
}
|
||||
|
|
|
@ -15,14 +15,11 @@
|
|||
*/
|
||||
package ghidra.feature.vt.api.db;
|
||||
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ADDRESS_SOURCE_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ASSOCIATION_KEY_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.DESTINATION_ADDRESS_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.MARKUP_TYPE_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ORIGINAL_DESTINATION_VALUE_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.SOURCE_ADDRESS_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.SOURCE_VALUE_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.STATUS_COL;
|
||||
import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import db.*;
|
||||
import ghidra.feature.vt.api.impl.MarkupItemStorage;
|
||||
import ghidra.feature.vt.api.main.VTSession;
|
||||
import ghidra.feature.vt.api.markuptype.VTMarkupTypeFactory;
|
||||
|
@ -34,10 +31,6 @@ import ghidra.program.model.listing.Program;
|
|||
import ghidra.util.exception.VersionException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import db.*;
|
||||
|
||||
public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBAdapter {
|
||||
|
||||
private Table table;
|
||||
|
@ -71,20 +64,20 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA
|
|||
|
||||
record.setLongValue(ASSOCIATION_KEY_COL.column(), association.getKey());
|
||||
record.setString(ADDRESS_SOURCE_COL.column(), markupItem.getDestinationAddressSource());
|
||||
record.setLongValue(SOURCE_ADDRESS_COL.column(), getAddressID(sourceProgram,
|
||||
markupItem.getSourceAddress()));
|
||||
record.setLongValue(SOURCE_ADDRESS_COL.column(),
|
||||
getAddressID(sourceProgram, markupItem.getSourceAddress()));
|
||||
|
||||
Address destinationAddress = markupItem.getDestinationAddress();
|
||||
if (destinationAddress != null) {
|
||||
record.setLongValue(DESTINATION_ADDRESS_COL.column(), getAddressID(destinationProgram,
|
||||
markupItem.getDestinationAddress()));
|
||||
record.setLongValue(DESTINATION_ADDRESS_COL.column(),
|
||||
getAddressID(destinationProgram, markupItem.getDestinationAddress()));
|
||||
}
|
||||
record.setShortValue(MARKUP_TYPE_COL.column(),
|
||||
(short) VTMarkupTypeFactory.getID(markupItem.getMarkupType()));
|
||||
record.setString(SOURCE_VALUE_COL.column(), Stringable.getString(
|
||||
markupItem.getSourceValue(), sourceProgram));
|
||||
record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(), Stringable.getString(
|
||||
markupItem.getDestinationValue(), destinationProgram));
|
||||
record.setString(SOURCE_VALUE_COL.column(),
|
||||
Stringable.getString(markupItem.getSourceValue(), sourceProgram));
|
||||
record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(),
|
||||
Stringable.getString(markupItem.getDestinationValue(), destinationProgram));
|
||||
record.setByteValue(STATUS_COL.column(), (byte) markupItem.getStatus().ordinal());
|
||||
|
||||
table.putRecord(record);
|
||||
|
@ -97,7 +90,7 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA
|
|||
}
|
||||
|
||||
@Override
|
||||
public void removeMatchMarkupItemRecord(long key) throws IOException {
|
||||
public void removeMarkupItemRecord(long key) throws IOException {
|
||||
table.deleteRecord(key);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ import org.jdom.JDOMException;
|
|||
import org.jdom.input.SAXBuilder;
|
||||
|
||||
import db.*;
|
||||
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
|
||||
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
||||
import ghidra.feature.vt.api.impl.*;
|
||||
import ghidra.feature.vt.api.main.*;
|
||||
import ghidra.framework.data.OpenMode;
|
||||
|
@ -186,37 +184,47 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet {
|
|||
|
||||
@Override
|
||||
public boolean removeMatch(VTMatch match) {
|
||||
if (!(match instanceof VTMatchDB)) {
|
||||
return false;
|
||||
}
|
||||
if (!match.getMatchSet().hasRemovableMatches()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VTMatchDB matchDB = (VTMatchDB) match;
|
||||
if (!(match instanceof VTMatchDB matchDb)) {
|
||||
// this should not be possible from the UI
|
||||
throw new IllegalArgumentException("Can only remove matches saved to the database");
|
||||
}
|
||||
|
||||
VTAssociation association = match.getAssociation();
|
||||
|
||||
// Remove the association if it was the only remaining match for that association.
|
||||
AssociationDatabaseManager associationManager = session.getAssociationManagerDBM();
|
||||
List<VTMatch> matches = session.getMatches(association);
|
||||
if (matches.size() == 1 && association.getStatus() == VTAssociationStatus.ACCEPTED) {
|
||||
return false; // can't remove the last match if the association is accepted
|
||||
// This method prevents deleting the association if it is accepted, as it would cause
|
||||
// the user to lose potentially valuable information without realizing it. To work
|
||||
// around that issue when calling this method, the user can first un-accept the match.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the match record
|
||||
deleteMatch(matchDb);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMatch(VTMatch match) {
|
||||
if (!(match instanceof VTMatchDB matchDb)) {
|
||||
// this should not be possible from the UI
|
||||
throw new IllegalArgumentException("Can only remove matches saved to the database");
|
||||
}
|
||||
|
||||
VTAssociation association = match.getAssociation();
|
||||
Address sourceAddress = association.getSourceAddress();
|
||||
Address destinationAddress = association.getDestinationAddress();
|
||||
try {
|
||||
lock.acquire();
|
||||
long matchKey = matchDB.getKey();
|
||||
long matchKey = matchDb.getKey();
|
||||
boolean deleted = matchTableAdapter.deleteRecord(matchKey);
|
||||
if (deleted) {
|
||||
matchCache.delete(matchKey);
|
||||
|
||||
if (matches.size() == 1) {
|
||||
List<VTMatch> matches = session.getMatches(association);
|
||||
if (matches.isEmpty()) {
|
||||
// if last match, remove association
|
||||
associationManager.removeAssociation(association);
|
||||
AssociationDatabaseManager manager = session.getAssociationManagerDBM();
|
||||
manager.removeAssociation(association);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -229,7 +237,6 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet {
|
|||
|
||||
DeletedMatch deletedMatch = new DeletedMatch(sourceAddress, destinationAddress);
|
||||
session.setObjectChanged(VTEvent.MATCH_DELETED, match, deletedMatch, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -349,14 +356,6 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRemovableMatches() {
|
||||
VTProgramCorrelatorInfo info = getProgramCorrelatorInfo();
|
||||
String correlatorClassName = info.getCorrelatorClassName();
|
||||
return correlatorClassName.equals(ManualMatchProgramCorrelator.class.getName()) ||
|
||||
correlatorClassName.equals(ImpliedMatchProgramCorrelator.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Match Set " + getID() + " - " + getMatchCount() + " matches [Correlator=" +
|
||||
|
|
|
@ -501,4 +501,8 @@ public class MarkupItemImpl implements VTMarkupItem {
|
|||
newStatus);
|
||||
}
|
||||
|
||||
// non-interface method
|
||||
public MarkupItemStorage getStorage() {
|
||||
return markupItemStorage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
*/
|
||||
package ghidra.feature.vt.api.impl;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ghidra.feature.vt.api.db.*;
|
||||
import ghidra.feature.vt.api.main.VTMarkupItem;
|
||||
import ghidra.feature.vt.api.main.VTMarkupItemStatus;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.util.task.TaskMonitorAdapter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MarkupItemManagerImpl {
|
||||
|
||||
|
@ -70,7 +70,7 @@ public class MarkupItemManagerImpl {
|
|||
return Collections.unmodifiableList(markupItems);
|
||||
}
|
||||
|
||||
protected List<VTMarkupItem> createMarkupItems(TaskMonitor monitor) throws CancelledException {
|
||||
private List<VTMarkupItem> createMarkupItems(TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
Collection<VTMarkupItem> generatedMarkupItems = getGeneratedMarkupItems(monitor);
|
||||
Collection<VTMarkupItem> databaseMarkupItems = getStoredMarkupItems(monitor);
|
||||
|
@ -104,18 +104,6 @@ public class MarkupItemManagerImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
private Collection<VTMarkupItem> getStoredMarkupItems(TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
AssociationDatabaseManager associationDBM = association.getAssociationManagerDB();
|
||||
Collection<MarkupItemStorageDB> appliedMarkupItems =
|
||||
associationDBM.getAppliedMarkupItems(monitor, association);
|
||||
List<VTMarkupItem> list = new ArrayList<VTMarkupItem>();
|
||||
for (MarkupItemStorageDB markupItemStorageDB : appliedMarkupItems) {
|
||||
list.add(new MarkupItemImpl(markupItemStorageDB));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<VTMarkupItem> replaceGeneratedMarkupItemsWithDBMarkupItems(
|
||||
Collection<VTMarkupItem> generatedMarkupItems,
|
||||
Collection<VTMarkupItem> databaseMarkupItems) {
|
||||
|
@ -142,8 +130,45 @@ public class MarkupItemManagerImpl {
|
|||
markupItem.getSourceAddress().toString(true);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
// synchronized due to write of 'markupItems'
|
||||
public synchronized void clearCache() {
|
||||
markupItems = EMPTY_LIST;
|
||||
}
|
||||
|
||||
private Collection<VTMarkupItem> getStoredMarkupItems(TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
AssociationDatabaseManager associationDBM = association.getAssociationManagerDB();
|
||||
Collection<MarkupItemStorageDB> appliedMarkupItems =
|
||||
associationDBM.getAppliedMarkupItems(monitor, association);
|
||||
List<VTMarkupItem> list = new ArrayList<VTMarkupItem>();
|
||||
for (MarkupItemStorageDB markupItemStorageDB : appliedMarkupItems) {
|
||||
list.add(new MarkupItemImpl(markupItemStorageDB));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// synchronized to match getMarkupItems() so we do not have other clients loading items while
|
||||
// we are processing them
|
||||
public synchronized void removeMarkupItems() {
|
||||
|
||||
List<VTMarkupItem> items;
|
||||
try {
|
||||
items = getMarkupItems(TaskMonitor.DUMMY);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
return; // can't happen with DUMMY
|
||||
}
|
||||
|
||||
List<MarkupItemImpl> impls = items.stream()
|
||||
.map(item -> (MarkupItemImpl) item)
|
||||
.filter(impl -> impl.isStoredInDB())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
AssociationDatabaseManager associationDbm = association.getAssociationManagerDB();
|
||||
associationDbm.removeStoredMarkupItems(impls);
|
||||
|
||||
// signal that markup item info has changed and must be reloaded when next needed
|
||||
clearCache();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,13 +15,11 @@
|
|||
*/
|
||||
package ghidra.feature.vt.api.impl;
|
||||
|
||||
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
|
||||
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.feature.vt.api.main.*;
|
||||
import ghidra.program.model.address.Address;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MatchSetImpl implements VTMatchSet {
|
||||
private ProgramCorrelatorInfoFake correlatorInfo;
|
||||
private final VTSession session;
|
||||
|
@ -77,11 +75,8 @@ public class MatchSetImpl implements VTMatchSet {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRemovableMatches() {
|
||||
VTProgramCorrelatorInfo info = getProgramCorrelatorInfo();
|
||||
String correlatorClassName = info.getCorrelatorClassName();
|
||||
return correlatorClassName.equals(ManualMatchProgramCorrelator.class.getName()) ||
|
||||
correlatorClassName.equals(ImpliedMatchProgramCorrelator.class.getName());
|
||||
public void deleteMatch(VTMatch match) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,11 +15,11 @@
|
|||
*/
|
||||
package ghidra.feature.vt.api.main;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import ghidra.feature.vt.api.impl.VTProgramCorrelatorInfo;
|
||||
import ghidra.program.model.address.Address;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Interface for all the matches generated from a single program correlator run.
|
||||
*
|
||||
|
@ -57,14 +56,14 @@ public interface VTMatchSet {
|
|||
|
||||
/**
|
||||
* Returns the number of matches contained in this match set.
|
||||
* @return
|
||||
* @return the number of matches contained in this match set.
|
||||
*/
|
||||
public int getMatchCount();
|
||||
|
||||
/**
|
||||
* Returns a unique id for this match set. The ids are one-up numbers indicating the order this
|
||||
* match set was generated in relation to other match sets in the VTSession.
|
||||
* @return
|
||||
* @return the id
|
||||
*/
|
||||
public int getID();
|
||||
|
||||
|
@ -72,7 +71,7 @@ public interface VTMatchSet {
|
|||
* Returns a collection of all matches for the given association.
|
||||
* @param association the association for which to search for matches.
|
||||
* @return a collection of all matches for the given association.
|
||||
* @see #getMatches(Address, Address, VTAssociationType)
|
||||
* @see #getMatches(Address, Address)
|
||||
*/
|
||||
public Collection<VTMatch> getMatches(VTAssociation association);
|
||||
|
||||
|
@ -90,16 +89,51 @@ public interface VTMatchSet {
|
|||
public Collection<VTMatch> getMatches(Address sourceAddress, Address destinationAddress);
|
||||
|
||||
/**
|
||||
* Removes a match from this match set. Note that this operation is only supported for built-in
|
||||
* match sets "Manual Matches" and "Implied Matches".
|
||||
* Deletes the given match from this match set.
|
||||
* <P>
|
||||
* Note: deleting an <B>ACCEPTED</B> match removes potentially useful corroborating evidence
|
||||
* from future correlation. Before deleting a match, consider instead filtering matches out of
|
||||
* the UI that you are finished applying.
|
||||
* <P>
|
||||
* If this is the last match that shares the match's association, then the association will also
|
||||
* be removed, along with any markup items in the database. <B>Any applied markup item data
|
||||
* will not be changed.</B>
|
||||
*
|
||||
* @param match the match
|
||||
*/
|
||||
public void deleteMatch(VTMatch match);
|
||||
|
||||
/**
|
||||
* Removes a match from this match set.
|
||||
* <P>
|
||||
* If this is the last match that shares the match's association, then the match will only be
|
||||
* removed if the association is not accepted. In that case, no remove will take place and
|
||||
* this method will return false.
|
||||
* <P>
|
||||
* Note: This method is deprecated. It unfortunately shares a very similar name with its
|
||||
* replacement, {@link #deleteMatch(VTMatch)}. The replacement method will delete the match
|
||||
* and the related association and markup items in the database, if the match is the last match
|
||||
* to use that association. This deprecated method does not remove the remaining association or
|
||||
* markup items. Historically, this method has been called after clearing the given match and
|
||||
* its markup. Once this method has been deleted, clients will be responsible for managing the
|
||||
* markup item state before calling {@link #deleteMatch(VTMatch)}.
|
||||
*
|
||||
* @param match the match to remove.
|
||||
* @return true if the match was removed.
|
||||
* @throws IllegalArgumentException if a non-database match is passed to this method
|
||||
* @see #deleteMatch(VTMatch)
|
||||
* @deprecated use {@link #deleteMatch(VTMatch)}
|
||||
*/
|
||||
@Deprecated(since = "11.2", forRemoval = true)
|
||||
public boolean removeMatch(VTMatch match);
|
||||
|
||||
/**
|
||||
* Returns true if this match set supports removing matches.
|
||||
* @return true if this match set supports removing matches.
|
||||
* Returns true
|
||||
* @return true
|
||||
* @deprecated this method now always returns true
|
||||
*/
|
||||
public boolean hasRemovableMatches();
|
||||
@Deprecated(since = "11.2", forRemoval = true)
|
||||
public default boolean hasRemovableMatches() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ public class RemoveMatchAction extends DockingAction {
|
|||
super("Remove", VTPlugin.OWNER);
|
||||
this.controller = controller;
|
||||
|
||||
// setToolBarData(new ToolBarData(ICON, MENU_GROUP));
|
||||
setPopupMenuData(new MenuData(new String[] { "Remove Match" }, ICON, MENU_GROUP));
|
||||
setEnabled(false);
|
||||
setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
|
||||
|
@ -64,17 +63,7 @@ public class RemoveMatchAction extends DockingAction {
|
|||
}
|
||||
VTMatchContext matchContext = (VTMatchContext) context;
|
||||
List<VTMatch> matches = matchContext.getSelectedMatches();
|
||||
if (matches.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!isRemovableMatch(matches.get(0))) {
|
||||
return false; // It must be a single manual match.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isRemovableMatch(VTMatch vtMatch) {
|
||||
return vtMatch.getMatchSet().hasRemovableMatches();
|
||||
return !matches.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -282,12 +282,6 @@ public abstract class AbstractAddressRangeFilter<T> extends AncillaryFilter<T>
|
|||
fireStatusChanged(getFilterStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
lowerAddressRangeTextField.setText(MIN_ADDRESS_VALUE.toString());
|
||||
upperAddressRangeTextField.setText(MAX_ADDRESS_VALUE.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getComponent() {
|
||||
return component;
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
*/
|
||||
package ghidra.feature.vt.gui.filters;
|
||||
|
||||
import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.APPLIED;
|
||||
import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.NONE;
|
||||
import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.*;
|
||||
|
||||
import java.awt.Container;
|
||||
import java.awt.LayoutManager;
|
||||
|
@ -151,13 +150,6 @@ public abstract class CheckBoxBasedAncillaryFilter<T> extends AncillaryFilter<T>
|
|||
return FilterShortcutState.REQUIRES_CHECK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
for (CheckBoxInfo<T> info : checkBoxInfos) {
|
||||
info.setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterState getFilterState() {
|
||||
FilterState state = new FilterState(this);
|
||||
|
|
|
@ -46,8 +46,6 @@ public abstract class Filter<T> {
|
|||
|
||||
public abstract FilterEditingStatus getFilterStatus();
|
||||
|
||||
public abstract void clearFilter();
|
||||
|
||||
public abstract JComponent getComponent();
|
||||
|
||||
public void dispose() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -18,12 +17,13 @@ package ghidra.feature.vt.gui.filters;
|
|||
|
||||
public interface FilterDialogModel<T> {
|
||||
|
||||
public void addFilter( Filter<T> filter );
|
||||
|
||||
public void forceRefilter();
|
||||
|
||||
/**
|
||||
* Will be called when the visibility of the dialog using this model has changed
|
||||
*/
|
||||
public void dialogVisibilityChanged( boolean isVisible );
|
||||
public void addFilter(Filter<T> filter);
|
||||
|
||||
public void forceRefilter();
|
||||
|
||||
/**
|
||||
* Will be called when the visibility of the dialog using this model has changed
|
||||
* @param isVisible true if visible
|
||||
*/
|
||||
public void dialogVisibilityChanged(boolean isVisible);
|
||||
}
|
||||
|
|
|
@ -243,12 +243,6 @@ public class TagFilter extends AncillaryFilter<VTMatch> {
|
|||
excludedTags = getTagsFromText(tagText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
excludedTags.clear();
|
||||
excludedTagsLabel.setText(ALL_TAGS_INCLUDED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getComponent() {
|
||||
return component;
|
||||
|
|
|
@ -150,13 +150,14 @@ public class VTPlugin extends Plugin {
|
|||
|
||||
private void addCustomPlugins() {
|
||||
|
||||
List<String> names = new ArrayList<>(List.of("ghidra.features.codecompare.plugin"));
|
||||
List<String> names =
|
||||
new ArrayList<>(List.of("ghidra.features.codecompare.plugin.FunctionComparisonPlugin"));
|
||||
List<Plugin> plugins = tool.getManagedPlugins();
|
||||
Set<String> existingNames =
|
||||
plugins.stream().map(c -> c.getName()).collect(Collectors.toSet());
|
||||
|
||||
// Note: we check to see if the plugins we want to add have already been added to the tool.
|
||||
// We should not needed to do this, but once the tool has been saved with the plugins added,
|
||||
// We should not need to do this, but once the tool has been saved with the plugins added,
|
||||
// they will get added again the next time the tool is loaded. Adding this check here seems
|
||||
// easier than modifying the default to file to load the plugins, since the amount of xml
|
||||
// required for that is non-trivial.
|
||||
|
|
|
@ -127,12 +127,6 @@ public abstract class AbstractDoubleRangeFilter<T> extends Filter<T>
|
|||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
lowerBoundField.setText(minValue.toString());
|
||||
upperBoundField.setText(maxValue.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterEditingStatus getFilterStatus() {
|
||||
FilterEditingStatus lowerStatus = lowerBoundField.getFilterStatus();
|
||||
|
|
|
@ -89,11 +89,6 @@ public class LengthFilter extends Filter<VTMatch> {
|
|||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
textField.setText(DEFAULT_FILTER_VALUE.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterEditingStatus getFilterStatus() {
|
||||
return textField.getFilterStatus();
|
||||
|
|
|
@ -34,6 +34,8 @@ import javax.swing.table.*;
|
|||
import docking.*;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import docking.widgets.table.*;
|
||||
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
||||
import docking.widgets.table.columnfilter.ColumnFilterManager;
|
||||
import docking.widgets.table.threaded.ThreadedTableModel;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.app.services.FunctionComparisonService;
|
||||
|
@ -80,6 +82,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
|||
|
||||
private AncillaryFilterDialogComponentProvider<VTMatch> ancillaryFilterDialog;
|
||||
private JButton ancillaryFilterButton;
|
||||
private ColumnFilterManager<VTMatch> columnFilterManager;
|
||||
private VTColumnFilter vtColumnFilter;
|
||||
|
||||
private FilterIconFlashTimer<VTMatch> iconTimer;
|
||||
private Set<Filter<VTMatch>> filters = new HashSet<>();
|
||||
|
@ -386,8 +390,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
|||
JPanel innerPanel = new JPanel(new HorizontalLayout(4));
|
||||
innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
|
||||
|
||||
JComponent nameFilterPanel = createTextFilterPanel();
|
||||
parentPanel.add(nameFilterPanel, BorderLayout.CENTER);
|
||||
JComponent textFilterPanel = createTextFilterPanel();
|
||||
parentPanel.add(textFilterPanel, BorderLayout.CENTER);
|
||||
parentPanel.add(innerPanel, BorderLayout.EAST);
|
||||
|
||||
JComponent scoreFilterPanel = createScoreFilterPanel();
|
||||
|
@ -409,13 +413,33 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
|||
helpService.registerHelp(parentPanel, filterHelpLocation);
|
||||
helpService.registerHelp(ancillaryFilterButton, filterHelpLocation);
|
||||
|
||||
JButton columnFilterButton = createColumnFilterButton();
|
||||
innerPanel.add(columnFilterButton);
|
||||
|
||||
innerPanel.add(ancillaryFilterButton);
|
||||
|
||||
return parentPanel;
|
||||
}
|
||||
|
||||
private JButton createColumnFilterButton() {
|
||||
|
||||
String preferenceKey =
|
||||
matchesTable.getPreferenceKey() + ColumnFilterManager.FILTER_EXTENSION;
|
||||
columnFilterManager = new ColumnFilterManager<VTMatch>(matchesTable, matchesTableModel,
|
||||
preferenceKey, this::updateColumnFilter);
|
||||
|
||||
vtColumnFilter = new VTColumnFilter(columnFilterManager.getCurrentFilter());
|
||||
addFilter(vtColumnFilter);
|
||||
|
||||
return columnFilterManager.getConfigureButton();
|
||||
}
|
||||
|
||||
private void updateColumnFilter() {
|
||||
vtColumnFilter.setFilter(columnFilterManager.getCurrentFilter());
|
||||
refilter();
|
||||
}
|
||||
|
||||
private JComponent createTextFilterPanel() {
|
||||
// MatchNameFilter nameFilterPanel = new MatchNameFilter(controller, matchesTable);
|
||||
AllTextFilter<VTMatch> allTextFilter =
|
||||
new AllTextFilter<>(controller, matchesTable, matchesTableModel);
|
||||
allTextFilter.setName(TEXT_FILTER_NAME);
|
||||
|
@ -506,6 +530,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
|||
}
|
||||
|
||||
ancillaryFilterDialog.dispose();
|
||||
|
||||
columnFilterManager.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1069,4 +1095,73 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
|||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private class VTColumnFilter extends Filter<VTMatch> {
|
||||
|
||||
private ColumnBasedTableFilter<VTMatch> columnFilter;
|
||||
|
||||
VTColumnFilter(ColumnBasedTableFilter<VTMatch> columnFilter) {
|
||||
this.columnFilter = columnFilter;
|
||||
}
|
||||
|
||||
void setFilter(ColumnBasedTableFilter<VTMatch> columnFilter) {
|
||||
this.columnFilter = columnFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passesFilter(VTMatch t) {
|
||||
if (columnFilter == null) {
|
||||
return true;
|
||||
}
|
||||
return columnFilter.acceptsRow(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterEditingStatus getFilterStatus() {
|
||||
if (columnFilter == null || columnFilter.isEmpty()) {
|
||||
return FilterEditingStatus.NONE;
|
||||
}
|
||||
|
||||
return FilterEditingStatus.APPLIED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getComponent() {
|
||||
// This filter is configured outside of the VT filter API
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterShortcutState getFilterShortcutState() {
|
||||
return FilterShortcutState.REQUIRES_CHECK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<VTMatch> createCopy() {
|
||||
return this; // does not currently support copying; should not be needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readConfigState(SaveState saveState) {
|
||||
// handled by the column filter manager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
// handled by the column filter manager
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSubFilterOf(Filter<VTMatch> otherFilter) {
|
||||
if (columnFilter == null) {
|
||||
return false;
|
||||
}
|
||||
if (otherFilter instanceof VTColumnFilter otherColumnFilter) {
|
||||
return columnFilter.isSubFilterOf(otherColumnFilter.columnFilter);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
*/
|
||||
package ghidra.feature.vt.gui.task;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.feature.vt.api.db.VTMatchSetDB;
|
||||
import ghidra.feature.vt.api.main.VTMatch;
|
||||
import ghidra.feature.vt.api.main.VTSession;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
|
@ -38,26 +40,75 @@ public class RemoveMatchTask extends VtTask {
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean removeMatches(TaskMonitor monitor) throws CancelledException {
|
||||
private void removeMatches(TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
monitor.setMessage("Removing matches");
|
||||
monitor.initialize(matches.size());
|
||||
boolean failed = false;
|
||||
for (VTMatch match : matches) {
|
||||
int n = matches.size();
|
||||
monitor.initialize(n);
|
||||
|
||||
//
|
||||
// First remove all matches that will not require user prompting (those that are not
|
||||
// accepted or they are not the last match for a shared association).
|
||||
//
|
||||
List<VTMatch> list = new ArrayList<>(matches); // create a mutable list
|
||||
Iterator<VTMatch> it = list.iterator();
|
||||
while (it.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
VTMatch match = it.next();
|
||||
VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
|
||||
boolean matchRemoved = matchSet.removeMatch(match);
|
||||
if (!matchRemoved) {
|
||||
failed = true;
|
||||
if (matchSet.removeMatch(match)) {
|
||||
it.remove();
|
||||
}
|
||||
monitor.incrementProgress(1);
|
||||
}
|
||||
|
||||
monitor.setProgress(matches.size());
|
||||
if (failed) {
|
||||
reportError("One or more of your matches could not be removed." +
|
||||
"\nNote: You can't remove a match if it is currently accepted.");
|
||||
if (list.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Now we have to ask the user if they wish to remove applied matches.
|
||||
//
|
||||
int delta = n - list.size();
|
||||
|
||||
//@formatter:off
|
||||
String message = """
|
||||
Deleted %d of %d matches.
|
||||
|
||||
The remaining %d matches are ACCEPTED. Do you wish to delete these matches and
|
||||
leave any applied destination program markup in place?
|
||||
(Press F1 to see more help details)
|
||||
""".formatted(delta, n, list.size());
|
||||
//@formatter:on
|
||||
|
||||
RemoveMatchDialog dialog = new RemoveMatchDialog(message);
|
||||
if (!dialog.promptToDelete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
it = list.iterator();
|
||||
while (it.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
VTMatch match = it.next();
|
||||
VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
|
||||
matchSet.deleteMatch(match);
|
||||
it.remove();
|
||||
monitor.incrementProgress(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private class RemoveMatchDialog extends OptionDialog {
|
||||
|
||||
RemoveMatchDialog(String message) {
|
||||
super("Delete ACCEPTED Matches?", message, "Delete Accepted Matches", "Finish",
|
||||
OptionDialog.QUESTION_MESSAGE, null, false);
|
||||
|
||||
setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
|
||||
}
|
||||
|
||||
boolean promptToDelete() {
|
||||
int choice = super.show();
|
||||
return choice == OptionDialog.OPTION_ONE; // "Delete Accepted Matches"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,11 +125,6 @@ public abstract class AbstractTextFilter<T> extends Filter<T> {
|
|||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFilter() {
|
||||
textField.setText(defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterEditingStatus getFilterStatus() {
|
||||
return textField.getFilterStatus();
|
||||
|
|
|
@ -115,7 +115,7 @@ public class ImpliedMatchUtils {
|
|||
VTMatchSet impliedMatchSet = session.getImpliedMatchSet();
|
||||
for (VTMatch vtMatch : matches) {
|
||||
if (vtMatch.getMatchSet() == impliedMatchSet) {
|
||||
impliedMatchSet.removeMatch(vtMatch);
|
||||
impliedMatchSet.deleteMatch(vtMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,8 +123,7 @@ public class ImpliedMatchUtils {
|
|||
/**
|
||||
* Method for finding version tracking implied matches given an accepted matched
|
||||
* function. Each referenced data and function that exist in equivalent sections
|
||||
* of the matched source and destination functions will added to the current
|
||||
* version tracking session as an implied match.
|
||||
* of the matched source and destination functions will be returned in the given set.
|
||||
*
|
||||
* @param sourceFunction The matched function from the source program
|
||||
* @param destinationFunction The matched function from the destination program
|
||||
|
|
|
@ -18,7 +18,7 @@ package ghidra.feature.vt.api;
|
|||
import static ghidra.feature.vt.db.VTTestUtils.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.*;
|
||||
|
||||
|
@ -27,9 +27,8 @@ import ghidra.feature.vt.api.main.VTAssociationStatus;
|
|||
import ghidra.feature.vt.api.main.VTMatch;
|
||||
import ghidra.feature.vt.gui.plugin.*;
|
||||
import ghidra.feature.vt.gui.task.AcceptMatchTask;
|
||||
import ghidra.feature.vt.gui.task.VtTask;
|
||||
import ghidra.feature.vt.gui.util.VTOptionDefines;
|
||||
import ghidra.framework.model.DomainObjectChangedEvent;
|
||||
import ghidra.framework.model.DomainObjectListener;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
|
@ -39,10 +38,7 @@ import ghidra.program.model.listing.*;
|
|||
import ghidra.program.model.symbol.*;
|
||||
import ghidra.program.model.util.CodeUnitInsertionException;
|
||||
import ghidra.test.*;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
import ghidra.util.task.Task;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
|
@ -53,13 +49,8 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
private ProgramDB sourceProgram;
|
||||
private ProgramDB destinationProgram;
|
||||
private VTPlugin plugin;
|
||||
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
|
||||
private Options options;
|
||||
|
||||
public VTMatchAcceptTest() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
|
@ -70,7 +61,6 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder();
|
||||
destinationProgram = destinationBuilder.getProgram();
|
||||
destinationProgram.addListener(eventRecorder);
|
||||
|
||||
tool = env.getTool();
|
||||
|
||||
|
@ -84,29 +74,21 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||
|
||||
options = controller.getOptions();
|
||||
options.setBoolean(VTOptionDefines.AUTO_CREATE_IMPLIED_MATCH, false);
|
||||
options.setBoolean(VTOptionDefines.APPLY_FUNCTION_NAME_ON_ACCEPT, false);
|
||||
options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, false);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
waitForBusyTool(tool);
|
||||
destinationProgram.flushEvents();
|
||||
waitForSwing();
|
||||
|
||||
env.dispose();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptWithApplyDataLabels() throws Exception {
|
||||
|
||||
//
|
||||
// BTW this test exposes a bug because the hook that runs when you apply data on accept was.
|
||||
// in a side effect, causing the destination address to be set. When the hook was changed to
|
||||
// not set the destination address, the accept task was not setting the destination address
|
||||
// as it should.
|
||||
// This test exposes a bug because the hook that runs when you apply data on accept was
|
||||
// exhibiting a side effect, causing the destination address to be set. When the hook was
|
||||
// changed to not set the destination address, the accept task was not setting the
|
||||
// destination address as it should.
|
||||
//
|
||||
|
||||
options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true);
|
||||
|
@ -147,11 +129,9 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
private void runTask(Task task) throws CancelledException {
|
||||
|
||||
task.run(TaskMonitor.DUMMY);
|
||||
destinationProgram.flushEvents();
|
||||
waitForSwing();
|
||||
private void runTask(VtTask task) {
|
||||
controller.runVTTask(task);
|
||||
waitForProgram(destinationProgram);
|
||||
}
|
||||
|
||||
private Data setData(DataType dataType, int dtLength, Address address, Program program)
|
||||
|
@ -170,14 +150,4 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private class DomainObjectListenerRecorder implements DomainObjectListener {
|
||||
|
||||
List<DomainObjectChangedEvent> events = new ArrayList<DomainObjectChangedEvent>();
|
||||
|
||||
@Override
|
||||
public void domainObjectChanged(DomainObjectChangedEvent ev) {
|
||||
events.add(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,6 @@ public class VTMatchApplyTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
private ProgramDB destinationProgram;
|
||||
private VTPlugin plugin;
|
||||
|
||||
// TODO: debug
|
||||
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
|
||||
|
||||
@Before
|
||||
|
|
|
@ -0,0 +1,354 @@
|
|||
/* ###
|
||||
* 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.feature.vt.api;
|
||||
|
||||
import static ghidra.feature.vt.db.VTTestUtils.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.*;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import ghidra.feature.vt.api.db.VTSessionDB;
|
||||
import ghidra.feature.vt.api.main.*;
|
||||
import ghidra.feature.vt.db.DummyTestProgramCorrelator;
|
||||
import ghidra.feature.vt.gui.plugin.*;
|
||||
import ghidra.feature.vt.gui.task.*;
|
||||
import ghidra.feature.vt.gui.util.VTOptionDefines;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.symbol.*;
|
||||
import ghidra.test.*;
|
||||
|
||||
public class VTMatchRemoveTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
private TestEnv env;
|
||||
private PluginTool tool;
|
||||
private VTController controller;
|
||||
private VTPlugin plugin;
|
||||
private VTSessionDB session;
|
||||
private ProgramDB srcProgram;
|
||||
private ProgramDB destProgram;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
env = new TestEnv();
|
||||
|
||||
ClassicSampleX86ProgramBuilder sourceBuilder = new ClassicSampleX86ProgramBuilder();
|
||||
srcProgram = sourceBuilder.getProgram();
|
||||
|
||||
ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder();
|
||||
destProgram = destinationBuilder.getProgram();
|
||||
|
||||
tool = env.getTool();
|
||||
tool.addPlugin(VTPlugin.class.getName());
|
||||
plugin = getPlugin(tool, VTPlugin.class);
|
||||
controller = new VTControllerImpl(plugin);
|
||||
|
||||
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||
srcProgram, destProgram, this);
|
||||
|
||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
env.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMatch_UnaccpetedMatch() throws Exception {
|
||||
|
||||
Address srcAddr = addr("0x0100808c", srcProgram);
|
||||
Address destAddr = addr("0x0100808c", destProgram);
|
||||
|
||||
setDataOnPrograms(srcAddr, destAddr);
|
||||
String labelName = "Bob";
|
||||
addLabel(labelName, srcAddr, srcProgram);
|
||||
|
||||
VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr);
|
||||
|
||||
VTMatchSet matchSet = match.getMatchSet();
|
||||
remove(match, false);
|
||||
assertMatchRemoved(matchSet, srcAddr, destAddr);
|
||||
assertNoLabelApplied(labelName, destAddr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMatch_AccpetedMatch() throws Exception {
|
||||
|
||||
/*
|
||||
Test:
|
||||
- create and apply a match
|
||||
- remove the match
|
||||
- leave the applied markup after match removal
|
||||
*/
|
||||
|
||||
Address srcAddr = addr("0x0100808c", srcProgram);
|
||||
Address destAddr = addr("0x0100808c", destProgram);
|
||||
|
||||
setDataOnPrograms(srcAddr, destAddr);
|
||||
String labelName = "Bob";
|
||||
addLabel(labelName, srcAddr, srcProgram);
|
||||
|
||||
VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr);
|
||||
setApplyDataLabelOnAccept();
|
||||
accept(match);
|
||||
assertAcceptedAndLabelApplied(match, labelName, destAddr);
|
||||
|
||||
VTMatchSet matchSet = match.getMatchSet();
|
||||
remove(match);
|
||||
assertMatchRemoved(matchSet, srcAddr, destAddr);
|
||||
assertLabelApplied(labelName, destAddr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMatch_Accepted_MultipleMatchesForAssociation() throws Exception {
|
||||
|
||||
/*
|
||||
Test:
|
||||
- create multiple matches for the same association
|
||||
- apply one match
|
||||
- remove the applied match
|
||||
- leave the applied markup after match removal
|
||||
|
||||
*This tests control flow that avoids execution when the match being removed is the last
|
||||
match for an association.
|
||||
|
||||
*/
|
||||
|
||||
Address srcAddr = addr("0x0100808c", srcProgram);
|
||||
Address destAddr = addr("0x0100808c", destProgram);
|
||||
|
||||
setDataOnPrograms(srcAddr, destAddr);
|
||||
String labelName = "Bob";
|
||||
addLabel(labelName, srcAddr, srcProgram);
|
||||
|
||||
VTMatch match = createMatchSetWithMultipleMatchesToSameAssociation(srcAddr, destAddr);
|
||||
setApplyDataLabelOnAccept();
|
||||
accept(match);
|
||||
assertAcceptedAndLabelApplied(match, labelName, destAddr);
|
||||
|
||||
VTMatchSet matchSet = match.getMatchSet();
|
||||
remove(match, false);
|
||||
assertMatchRemoved(matchSet, srcAddr, destAddr);
|
||||
assertLabelApplied(labelName, destAddr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMatch_RejectedMatch() throws Exception {
|
||||
|
||||
Address srcAddr = addr("0x0100808c", srcProgram);
|
||||
Address destAddr = addr("0x0100808c", destProgram);
|
||||
|
||||
setDataOnPrograms(srcAddr, destAddr);
|
||||
String labelName = "Bob";
|
||||
addLabel(labelName, srcAddr, srcProgram);
|
||||
|
||||
VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr);
|
||||
setApplyDataLabelOnAccept();
|
||||
reject(match);
|
||||
assertNoLabelApplied(labelName, destAddr);
|
||||
|
||||
VTMatchSet matchSet = match.getMatchSet();
|
||||
remove(match, false);
|
||||
assertMatchRemoved(matchSet, srcAddr, destAddr);
|
||||
assertNoLabelApplied(labelName, destAddr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMatch_AccpetedMatch_ChooseNotToDelete() throws Exception {
|
||||
|
||||
/*
|
||||
Test:
|
||||
- create and apply a match
|
||||
- remove the match, but cancel at dialog prompt
|
||||
- match should still be valid; markup should still be applied
|
||||
*/
|
||||
|
||||
Address srcAddr = addr("0x0100808c", srcProgram);
|
||||
Address destAddr = addr("0x0100808c", destProgram);
|
||||
|
||||
setDataOnPrograms(srcAddr, destAddr);
|
||||
String labelName = "Bob";
|
||||
addLabel(labelName, srcAddr, srcProgram);
|
||||
|
||||
VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr);
|
||||
setApplyDataLabelOnAccept();
|
||||
accept(match);
|
||||
assertAcceptedAndLabelApplied(match, labelName, destAddr);
|
||||
|
||||
VTMatchSet matchSet = match.getMatchSet();
|
||||
startRemoveThenCancel(match);
|
||||
assertMatchNotRemoved(matchSet, srcAddr, destAddr);
|
||||
assertLabelApplied(labelName, destAddr);
|
||||
}
|
||||
|
||||
//=================================================================================================
|
||||
// Private Methods
|
||||
//=================================================================================================
|
||||
|
||||
private void remove(VTMatch match) {
|
||||
remove(match, true);
|
||||
}
|
||||
|
||||
private void remove(VTMatch match, boolean expectPrompt) {
|
||||
RemoveMatchTask task = new RemoveMatchTask(session, List.of(match));
|
||||
|
||||
AtomicBoolean finished = runTaskLater(task); // this task is blocking, so run later and wait
|
||||
|
||||
if (expectPrompt) {
|
||||
DialogComponentProvider removeDialog =
|
||||
waitForDialogComponent("Delete ACCEPTED Matches?");
|
||||
pressButtonByText(removeDialog, "Delete Accepted Matches");
|
||||
}
|
||||
|
||||
// let the task finish processing after pressing the button
|
||||
waitFor(finished);
|
||||
waitForProgram(destProgram);
|
||||
}
|
||||
|
||||
private void startRemoveThenCancel(VTMatch match) {
|
||||
RemoveMatchTask task = new RemoveMatchTask(session, List.of(match));
|
||||
|
||||
AtomicBoolean finished = runTaskLater(task); // this task is blocking, so run later and wait
|
||||
|
||||
DialogComponentProvider removeDialog = waitForDialogComponent("Delete ACCEPTED Matches?");
|
||||
pressButtonByText(removeDialog, "Finish");
|
||||
|
||||
// let the task finish processing after pressing the button
|
||||
waitFor(finished);
|
||||
waitForProgram(destProgram);
|
||||
}
|
||||
|
||||
private void assertLabelApplied(String labelName, Address addr) {
|
||||
assertEquals(labelName, getSymbol(destProgram, addr).getName());
|
||||
}
|
||||
|
||||
private void assertNoLabelApplied(String labelName, Address addr) {
|
||||
Symbol symbol = getSymbol(destProgram, addr);
|
||||
if (symbol == null) {
|
||||
return; // no label; expected
|
||||
}
|
||||
assertNotEquals(labelName, symbol.getName());
|
||||
}
|
||||
|
||||
private void assertMatchRemoved(VTMatchSet matchSet, Address srcAddr, Address destAddr) {
|
||||
Collection<VTMatch> matches = matchSet.getMatches(srcAddr, destAddr);
|
||||
assertTrue(matches.isEmpty());
|
||||
}
|
||||
|
||||
private void assertMatchNotRemoved(VTMatchSet matchSet, Address srcAddr, Address destAddr) {
|
||||
Collection<VTMatch> matches = matchSet.getMatches(srcAddr, destAddr);
|
||||
assertFalse(matches.isEmpty());
|
||||
}
|
||||
|
||||
private void assertAcceptedAndLabelApplied(VTMatch match, String labelName, Address addr) {
|
||||
VTAssociationStatus status = match.getAssociation().getStatus();
|
||||
assertEquals(VTAssociationStatus.ACCEPTED, status);
|
||||
assertEquals(labelName, getSymbol(destProgram, addr).getName());
|
||||
}
|
||||
|
||||
private Symbol getSymbol(Program p, Address addr) {
|
||||
return p.getSymbolTable().getPrimarySymbol(addr);
|
||||
}
|
||||
|
||||
private void setApplyDataLabelOnAccept() {
|
||||
ToolOptions options = controller.getOptions();
|
||||
options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true);
|
||||
}
|
||||
|
||||
private void accept(VTMatch match) throws Exception {
|
||||
AcceptMatchTask task = new AcceptMatchTask(controller, List.of(match));
|
||||
runTask(task);
|
||||
}
|
||||
|
||||
private void reject(VTMatch match) throws Exception {
|
||||
RejectMatchTask task = new RejectMatchTask(session, List.of(match));
|
||||
runTask(task);
|
||||
}
|
||||
|
||||
private void setDataOnPrograms(Address srcAddr, Address destAddr) {
|
||||
DataType srcDt = new DWordDataType();
|
||||
DataType destDt1 = new StringDataType();
|
||||
DataType destDt2 = new WordDataType();
|
||||
setData(srcDt, 4, srcAddr, srcProgram);
|
||||
setData(destDt1, 2, destAddr, destProgram);
|
||||
setData(destDt2, 2, destAddr.add(2), destProgram);
|
||||
}
|
||||
|
||||
private Symbol addLabel(String name, Address address, Program program) {
|
||||
return tx(program, () -> {
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
return symbolTable.createLabel(address, name, SourceType.USER_DEFINED);
|
||||
});
|
||||
}
|
||||
|
||||
private Data setData(DataType dataType, int length, Address address, Program program) {
|
||||
return tx(program, () -> {
|
||||
Listing listing = program.getListing();
|
||||
return listing.createData(address, dataType, length);
|
||||
});
|
||||
}
|
||||
|
||||
private AtomicBoolean runTaskLater(VtTask task) {
|
||||
AtomicBoolean finishedFlag = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
controller.runVTTask(task);
|
||||
finishedFlag.set(true);
|
||||
});
|
||||
waitForSwing();
|
||||
return finishedFlag;
|
||||
}
|
||||
|
||||
private void runTask(VtTask task) {
|
||||
controller.runVTTask(task);
|
||||
waitForProgram(destProgram);
|
||||
}
|
||||
|
||||
private VTMatch createMatchSetWithMultipleMatchesToSameAssociation(Address srcAddr,
|
||||
Address destAddr) throws Exception {
|
||||
int txId = 0;
|
||||
try {
|
||||
txId = session.startTransaction("Test Create Data Match Set");
|
||||
VTMatchInfo info = createRandomMatch(srcAddr, destAddr, session);
|
||||
info.setAssociationType(VTAssociationType.DATA);
|
||||
VTMatchSet matchSet =
|
||||
session.createMatchSet(createProgramCorrelator(srcProgram, destProgram));
|
||||
VTMatch firstMatch = matchSet.addMatch(info);
|
||||
|
||||
// create a second match, match set and correlator, all tied to the given association
|
||||
DummyTestProgramCorrelator pc2 =
|
||||
(DummyTestProgramCorrelator) createProgramCorrelator(srcProgram, destProgram);
|
||||
pc2.setName("Correlator Two");
|
||||
VTMatchSet ms2 = session.createMatchSet(pc2);
|
||||
ms2.addMatch(createRandomMatch(srcAddr, destAddr, session));
|
||||
|
||||
return firstMatch;
|
||||
}
|
||||
finally {
|
||||
session.endTransaction(txId, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import ghidra.util.task.TaskMonitor;
|
|||
|
||||
public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
|
||||
|
||||
private String name = "DummyTestProgramCorrelator";
|
||||
private int matchCount = 1;
|
||||
|
||||
public DummyTestProgramCorrelator() {
|
||||
|
@ -84,8 +85,12 @@ public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
|
|||
}
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DummyTestProgramCorrelator";
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
|||
assertEquals(VTEvent.MATCH_ADDED, events.get(0).getEventType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal") // ignore the warning until removeMatch() is removed
|
||||
@Test
|
||||
public void testEventsForRemovingLastMatchForAssociation() {
|
||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||
|
@ -134,6 +135,22 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
|||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventsForDeletingLastMatchForAssociation() {
|
||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||
clearEvents();
|
||||
VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null);
|
||||
VTMatch match = manualMatchSet.addMatch(matchInfo);
|
||||
clearEvents();
|
||||
|
||||
manualMatchSet.deleteMatch(match);
|
||||
|
||||
assertEventCount(2);
|
||||
assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType());
|
||||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal") // ignore the warning until removeMatch() is removed
|
||||
@Test
|
||||
public void testEventsForRemovingNonLastMatchForAssociation() {
|
||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||
|
@ -149,6 +166,21 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
|||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventsForDeletingNonLastMatchForAssociation() {
|
||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||
clearEvents();
|
||||
VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null);
|
||||
VTMatch match = manualMatchSet.addMatch(matchInfo);
|
||||
clearEvents();
|
||||
|
||||
manualMatchSet.deleteMatch(match);
|
||||
|
||||
assertEventCount(2);
|
||||
assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType());
|
||||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventsForRejectingMatch() throws VTAssociationStatusException {
|
||||
VTMatchSet matchSet = createMatchSet();
|
||||
|
|
|
@ -24,29 +24,21 @@ import javax.swing.*;
|
|||
import javax.swing.border.BevelBorder;
|
||||
import javax.swing.event.*;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
import javax.swing.table.TableModel;
|
||||
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.menu.*;
|
||||
import docking.widgets.EmptyBorderButton;
|
||||
import docking.widgets.EventTrigger;
|
||||
import docking.widgets.filter.*;
|
||||
import docking.widgets.label.GDLabel;
|
||||
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
||||
import docking.widgets.table.columnfilter.ColumnFilterSaveManager;
|
||||
import docking.widgets.table.constraint.dialog.ColumnFilterDialog;
|
||||
import generic.theme.GIcon;
|
||||
import docking.widgets.table.columnfilter.ColumnFilterManager;
|
||||
import ghidra.framework.options.PreferenceState;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||
import ghidra.util.datastruct.WeakSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import help.HelpService;
|
||||
import resources.Icons;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
import utility.function.Callback;
|
||||
|
||||
|
@ -112,37 +104,26 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield";
|
||||
private static final String FILTER_STATE = "FILTER_STATE";
|
||||
private static final String FILTER_EXTENSION = ".FilterExtension";
|
||||
private static final Icon FILTER_ON_ICON = new GIcon("icon.widget.filterpanel.filter.on");
|
||||
private static final Icon FILTER_OFF_ICON = new GIcon("icon.widget.filterpanel.filter.off");
|
||||
private static final Icon APPLY_FILTER_ICON = Icons.OPEN_FOLDER_ICON;
|
||||
private static final Icon CLEAR_FILTER_ICON = Icons.DELETE_ICON;
|
||||
|
||||
private JTable table;
|
||||
private RowObjectFilterModel<ROW_OBJECT> textFilterModel;
|
||||
private RowObjectFilterModel<ROW_OBJECT> rowObjectFilterModel;
|
||||
private JLabel searchLabel;
|
||||
|
||||
private FilterTextField filterField;
|
||||
private FilterListener filterListener = new GTableFilterListener();
|
||||
|
||||
private WeakSet<Callback> listeners =
|
||||
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
|
||||
|
||||
private FilterOptions filterOptions = new FilterOptions();
|
||||
private TableTextFilterFactory<ROW_OBJECT> filterFactory =
|
||||
new DefaultTableTextFilterFactory<>(filterOptions);
|
||||
private RowFilterTransformer<ROW_OBJECT> transformer;
|
||||
private TableFilter<ROW_OBJECT> secondaryTableFilter;
|
||||
private ColumnBasedTableFilter<ROW_OBJECT> columnTableFilter;
|
||||
private List<ColumnBasedTableFilter<ROW_OBJECT>> savedFilters = new ArrayList<>();
|
||||
private EmptyBorderButton filterStateButton;
|
||||
|
||||
private ColumnFilterManager<ROW_OBJECT> columnFilterManager;
|
||||
|
||||
private String uniquePreferenceKey;
|
||||
|
||||
private MultiStateDockingAction<ColumnBasedTableFilter<ROW_OBJECT>> columnFilterAction;
|
||||
private ColumnFilterDialog<ROW_OBJECT> columnFilterDialog;
|
||||
private ColumnBasedTableFilter<ROW_OBJECT> lastUsedColumnFilter;
|
||||
|
||||
private SwingUpdateManager updateManager = new SwingUpdateManager(250, 1000, () -> {
|
||||
private SwingUpdateManager filterUpdater = new SwingUpdateManager(250, 1000, () -> {
|
||||
String text = filterField.getText();
|
||||
TableFilter<ROW_OBJECT> tableFilter = filterFactory.getTableFilter(text, transformer);
|
||||
|
||||
|
@ -151,8 +132,9 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
// result of a filter, the table does not know this and may update the wrong row data.
|
||||
table.editingCanceled(null);
|
||||
|
||||
textFilterModel.setTableFilter(
|
||||
getCombinedTableFilter(secondaryTableFilter, tableFilter, columnTableFilter));
|
||||
ColumnBasedTableFilter<ROW_OBJECT> columnFilter = columnFilterManager.getCurrentFilter();
|
||||
rowObjectFilterModel.setTableFilter(
|
||||
getCombinedTableFilter(secondaryTableFilter, tableFilter, columnFilter));
|
||||
});
|
||||
|
||||
/** I'm a field so that my weak reference won't go away */
|
||||
|
@ -174,12 +156,12 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
|
||||
@Override
|
||||
public void columnRemoved(TableColumnModelEvent e) {
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void columnAdded(TableColumnModelEvent e) {
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -231,13 +213,16 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
String filterLabel) {
|
||||
this.table = table;
|
||||
|
||||
buildPanel(filterLabel);
|
||||
|
||||
uniquePreferenceKey = createUniqueFilterPreferenceKey(table);
|
||||
|
||||
transformer = new DefaultRowFilterTransformer<>(tableModel, table.getColumnModel());
|
||||
|
||||
textFilterModel = installTableModel(tableModel);
|
||||
rowObjectFilterModel = installTableModel(tableModel);
|
||||
|
||||
columnFilterManager = new ColumnFilterManager<ROW_OBJECT>(table, rowObjectFilterModel,
|
||||
getPreferenceKey(), filterUpdater::updateLater);
|
||||
|
||||
buildPanel(filterLabel);
|
||||
|
||||
TableColumnModel columnModel = table.getColumnModel();
|
||||
columnModel.addColumnModelListener(columnModelListener);
|
||||
|
@ -246,12 +231,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
table.addPropertyChangeListener(badProgrammingPropertyChangeListener);
|
||||
|
||||
DockingWindowManager.registerComponentLoadedListener(this,
|
||||
(windowManager, provider) -> initialize(windowManager));
|
||||
}
|
||||
|
||||
private void initialize(DockingWindowManager windowManager) {
|
||||
loadFilterPreference(windowManager);
|
||||
initializeSavedFilters();
|
||||
(windowManager, provider) -> loadFilterPreference(windowManager));
|
||||
}
|
||||
|
||||
private void loadFilterPreference(DockingWindowManager dockingWindowManager) {
|
||||
|
@ -283,7 +263,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
if (xmlElement != null) {
|
||||
this.filterOptions = FilterOptions.restoreFromXML(xmlElement);
|
||||
updateFilterFactory();
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -326,18 +306,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
* @param newFilter the ColumnTableFilter to use for filtering this table.
|
||||
*/
|
||||
public void setColumnTableFilter(ColumnBasedTableFilter<ROW_OBJECT> newFilter) {
|
||||
if (Objects.equals(newFilter, this.columnTableFilter)) {
|
||||
return;
|
||||
}
|
||||
if (columnTableFilter != null && !columnTableFilter.isSaved()) {
|
||||
lastUsedColumnFilter = columnTableFilter;
|
||||
}
|
||||
columnTableFilter = newFilter;
|
||||
updateTableContents();
|
||||
updateColumnFilterButton();
|
||||
if (columnFilterDialog != null) {
|
||||
columnFilterDialog.filterChanged(newFilter);
|
||||
}
|
||||
columnFilterManager.setFilter(newFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -351,7 +320,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
*/
|
||||
public void setFilterRowTransformer(RowFilterTransformer<ROW_OBJECT> transformer) {
|
||||
this.transformer = transformer;
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -362,7 +331,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
*/
|
||||
public void setSecondaryFilter(TableFilter<ROW_OBJECT> tableFilter) {
|
||||
this.secondaryTableFilter = tableFilter;
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -373,7 +342,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
public void setFilterOptions(FilterOptions filterOptions) {
|
||||
this.filterOptions = filterOptions;
|
||||
updateFilterFactory();
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
doSaveState();
|
||||
}
|
||||
|
||||
|
@ -394,7 +363,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
add(buildFilterStateButton());
|
||||
if (isTableColumnFilterableModel()) {
|
||||
add(Box.createHorizontalStrut(5));
|
||||
add(buildColumnFilterStateButton());
|
||||
add(columnFilterManager.getConfigureButton());
|
||||
}
|
||||
|
||||
HelpService helpService = DockingWindowManager.getHelpService();
|
||||
|
@ -425,107 +394,6 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
return table.getModel() instanceof RowObjectFilterModel;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private JComponent buildColumnFilterStateButton() {
|
||||
|
||||
RowObjectFilterModel<ROW_OBJECT> tableModel =
|
||||
(RowObjectFilterModel<ROW_OBJECT>) table.getModel();
|
||||
columnFilterAction =
|
||||
new NonToolbarMultiStateAction<>("Column Filter", "GTableFilterPanel") {
|
||||
|
||||
@Override
|
||||
public void actionStateChanged(
|
||||
ActionState<ColumnBasedTableFilter<ROW_OBJECT>> newActionState,
|
||||
EventTrigger trigger) {
|
||||
if (trigger != EventTrigger.GUI_ACTION) {
|
||||
return;
|
||||
}
|
||||
ColumnFilterActionState state = (ColumnFilterActionState) newActionState;
|
||||
state.performAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed() {
|
||||
showFilterDialog(tableModel);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
HelpLocation helpLocation = new HelpLocation("Trees", "Column_Filters");
|
||||
columnFilterAction.setHelpLocation(helpLocation);
|
||||
|
||||
updateFilterFactory();
|
||||
updateColumnFilterButton();
|
||||
JButton button = columnFilterAction.createButton();
|
||||
DockingWindowManager.getHelpService().registerHelp(button, helpLocation);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void initializeSavedFilters() {
|
||||
TableModel model = table.getModel();
|
||||
if (!(model instanceof GDynamicColumnTableModel)) {
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
GDynamicColumnTableModel<ROW_OBJECT, ?> dynamicModel =
|
||||
(GDynamicColumnTableModel<ROW_OBJECT, ?>) model;
|
||||
|
||||
ColumnFilterSaveManager<ROW_OBJECT> saveManager =
|
||||
new ColumnFilterSaveManager<>(this, table, dynamicModel, dynamicModel.getDataSource());
|
||||
savedFilters = saveManager.getSavedFilters();
|
||||
Collections.reverse(savedFilters);
|
||||
updateColumnFilterButton();
|
||||
}
|
||||
|
||||
private void updateColumnFilterButton() {
|
||||
List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> list = getActionStates();
|
||||
|
||||
columnFilterAction.setActionStates(list);
|
||||
}
|
||||
|
||||
private List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> getActionStates() {
|
||||
List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> list = new ArrayList<>();
|
||||
if (columnTableFilter == null) {
|
||||
list.add(new CreateFilterActionState());
|
||||
}
|
||||
else {
|
||||
list.add(new EditFilterActionState(columnTableFilter));
|
||||
list.add(new ClearFilterActionState());
|
||||
}
|
||||
if (lastUsedColumnFilter != null) {
|
||||
list.add(new ApplyLastUsedActionState(lastUsedColumnFilter));
|
||||
}
|
||||
for (ColumnBasedTableFilter<ROW_OBJECT> filter : savedFilters) {
|
||||
list.add(new ApplyFilterActionState(filter));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void showFilterDialog(RowObjectFilterModel<ROW_OBJECT> tableModel) {
|
||||
if (columnFilterDialog == null) {
|
||||
if (ColumnFilterDialog.hasFilterableColumns(table, tableModel)) {
|
||||
DockingWindowManager dockingWindowManager = DockingWindowManager.getInstance(table);
|
||||
loadFilterPreference(dockingWindowManager);
|
||||
columnFilterDialog = new ColumnFilterDialog<>(this, table, tableModel);
|
||||
}
|
||||
else {
|
||||
Msg.showError(this, this, "Column Filter Error",
|
||||
"This table contains no filterable columns!");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
columnFilterDialog.setCloseCallback(() -> {
|
||||
doSaveState();
|
||||
updateFilterFactory();
|
||||
columnFilterDialog = null;
|
||||
});
|
||||
|
||||
DockingWindowManager.showDialog(GTableFilterPanel.this, columnFilterDialog);
|
||||
}
|
||||
|
||||
private void updateFilterFactory() {
|
||||
filterStateButton.setIcon(filterOptions.getFilterStateIcon());
|
||||
filterStateButton.setToolTipText(filterOptions.getFilterDescription());
|
||||
|
@ -587,17 +455,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
}
|
||||
|
||||
public RowObjectFilterModel<ROW_OBJECT> getTableFilterModel() {
|
||||
return textFilterModel;
|
||||
}
|
||||
|
||||
/** Convenience method to refilter the table's contents */
|
||||
private void updateTableContents() {
|
||||
updateManager.updateLater();
|
||||
notifyFilterChanged();
|
||||
}
|
||||
|
||||
private void notifyFilterChanged() {
|
||||
listeners.forEach(callback -> callback.call());
|
||||
return rowObjectFilterModel;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
@ -609,13 +467,11 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
columnModel.removeColumnModelListener(columnModelListener);
|
||||
columnModelListener = null;
|
||||
|
||||
if (columnFilterDialog != null) {
|
||||
columnFilterDialog.dispose();
|
||||
}
|
||||
columnFilterManager.dispose();
|
||||
|
||||
table.removePropertyChangeListener(badProgrammingPropertyChangeListener);
|
||||
|
||||
updateManager.dispose();
|
||||
filterUpdater.dispose();
|
||||
if (table instanceof GTable) {
|
||||
((GTable) table).dispose();
|
||||
}
|
||||
|
@ -696,7 +552,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
return viewRow;
|
||||
}
|
||||
|
||||
return textFilterModel.getModelRow(viewRow);
|
||||
return rowObjectFilterModel.getModelRow(viewRow);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -710,7 +566,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
* @return the row in the table for the given model row.
|
||||
*/
|
||||
public int getViewRow(int modelRow) {
|
||||
return textFilterModel.getViewRow(modelRow);
|
||||
return rowObjectFilterModel.getViewRow(modelRow);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -720,7 +576,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
* @return the row object matching the given index
|
||||
*/
|
||||
public ROW_OBJECT getRowObject(int viewRow) {
|
||||
ROW_OBJECT rowObject = textFilterModel.getRowObject(viewRow);
|
||||
ROW_OBJECT rowObject = rowObjectFilterModel.getRowObject(viewRow);
|
||||
return rowObject;
|
||||
}
|
||||
|
||||
|
@ -736,7 +592,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
return;
|
||||
}
|
||||
|
||||
int viewRow = textFilterModel.getViewIndex(t);
|
||||
int viewRow = rowObjectFilterModel.getViewIndex(t);
|
||||
if (viewRow >= 0) {
|
||||
table.setRowSelectionInterval(viewRow, viewRow);
|
||||
scrollToSelectedRow();
|
||||
|
@ -785,7 +641,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
if (row < 0) {
|
||||
return null;
|
||||
}
|
||||
return textFilterModel.getRowObject(row);
|
||||
return rowObjectFilterModel.getRowObject(row);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -801,7 +657,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
|
||||
List<ROW_OBJECT> list = new ArrayList<>(rows.length);
|
||||
for (int row : rows) {
|
||||
list.add(textFilterModel.getRowObject(row));
|
||||
list.add(rowObjectFilterModel.getRowObject(row));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
@ -814,7 +670,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
* @return true if in the view
|
||||
*/
|
||||
public boolean isInView(ROW_OBJECT o) {
|
||||
int rowIndex = textFilterModel.getRowIndex(o);
|
||||
int rowIndex = rowObjectFilterModel.getRowIndex(o);
|
||||
return rowIndex >= 0;
|
||||
}
|
||||
|
||||
|
@ -823,11 +679,48 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
}
|
||||
|
||||
public int getRowCount() {
|
||||
return textFilterModel.getRowCount();
|
||||
return rowObjectFilterModel.getRowCount();
|
||||
}
|
||||
|
||||
public int getUnfilteredRowCount() {
|
||||
return textFilterModel.getUnfilteredRowCount();
|
||||
return rowObjectFilterModel.getUnfilteredRowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key used to store user filter configuration state. You can override this
|
||||
* method to generate unique keys yourself. You are required to override this method if
|
||||
* you create multiple versions of a filter panel from the same place in your code, as
|
||||
* multiple instances created in the same place will cause them all to share the same key and
|
||||
* thus to have the same filter settings when they are created initially.
|
||||
* <p>
|
||||
* As an example, consider a plugin that creates <code>n</code> providers. If each provider uses
|
||||
* a filter panel, then each provider will share the same filter settings when that provider
|
||||
* is created. If this is not what you want, then you need to override this method to
|
||||
* generate a unique key for each provider.
|
||||
*
|
||||
* @param jTable the table
|
||||
* @return a key used to store user filter configuration state.
|
||||
*/
|
||||
public String createUniqueFilterPreferenceKey(JTable jTable) {
|
||||
return generateFilterPreferenceKey(jTable, FILTER_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ColumnTableFilter that has been set on this GTableFilterPanel or null if there
|
||||
* is none.
|
||||
*
|
||||
* @return the ColumnTableFilter that has been set.
|
||||
*/
|
||||
public ColumnBasedTableFilter<ROW_OBJECT> getColumnTableFilter() {
|
||||
return columnFilterManager.getCurrentFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique key that can be used to store preferences for this table.
|
||||
* @return a unique key that can be used to store preferences for this table.
|
||||
*/
|
||||
public String getPreferenceKey() {
|
||||
return uniquePreferenceKey;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
|
@ -900,7 +793,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
}
|
||||
|
||||
private TableModelEvent translateEventForFilter(TableModelEvent event) {
|
||||
int rowCount = textFilterModel.getUnfilteredRowCount();
|
||||
int rowCount = rowObjectFilterModel.getUnfilteredRowCount();
|
||||
if (rowCount == 0) {
|
||||
return event; // nothing to translate--no data
|
||||
}
|
||||
|
@ -915,14 +808,14 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
|
||||
if (firstRow == 0 && lastRow == rowCount - 1) {
|
||||
firstRow = 0;
|
||||
lastRow = Math.max(0, textFilterModel.getRowCount() - 1);
|
||||
lastRow = Math.max(0, rowObjectFilterModel.getRowCount() - 1);
|
||||
}
|
||||
else {
|
||||
// translate to the filtered view (from the wrapped model's full universe)
|
||||
firstRow = getViewRow(firstRow);
|
||||
lastRow = getViewRow(lastRow);
|
||||
}
|
||||
return new TableModelEvent(textFilterModel, firstRow, lastRow, event.getColumn(),
|
||||
return new TableModelEvent(rowObjectFilterModel, firstRow, lastRow, event.getColumn(),
|
||||
event.getType());
|
||||
}
|
||||
}
|
||||
|
@ -937,8 +830,8 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
}
|
||||
|
||||
isUpdatingModel = true;
|
||||
if (textFilterModel instanceof WrappingTableModel) {
|
||||
WrappingTableModel tableModelWrapper = (WrappingTableModel) textFilterModel;
|
||||
if (rowObjectFilterModel instanceof WrappingTableModel) {
|
||||
WrappingTableModel tableModelWrapper = (WrappingTableModel) rowObjectFilterModel;
|
||||
tableModelWrapper.wrappedModelChangedFromTableChangedEvent();
|
||||
}
|
||||
filterField.alert();
|
||||
|
@ -947,75 +840,16 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
}
|
||||
|
||||
private class GTableFilterListener implements FilterListener {
|
||||
|
||||
@Override
|
||||
public void filterChanged(String text) {
|
||||
updateTableContents();
|
||||
filterUpdater.updateLater();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key used to store user filter configuration state. You can override this
|
||||
* method to generate unique keys yourself. You are required to override this method if
|
||||
* you create multiple versions of a filter panel from the same place in your code, as
|
||||
* multiple instances created in the same place will cause them all to share the same key and
|
||||
* thus to have the same filter settings when they are created initially.
|
||||
* <p>
|
||||
* As an example, consider a plugin that creates <code>n</code> providers. If each provider uses
|
||||
* a filter panel, then each provider will share the same filter settings when that provider
|
||||
* is created. If this is not what you want, then you need to override this method to
|
||||
* generate a unique key for each provider.
|
||||
*
|
||||
* @param jTable the table
|
||||
* @return a key used to store user filter configuration state.
|
||||
*/
|
||||
public String createUniqueFilterPreferenceKey(JTable jTable) {
|
||||
return generateFilterPreferenceKey(jTable, FILTER_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ColumnTableFilter that has been set on this GTableFilterPanel or null if there
|
||||
* is none.
|
||||
*
|
||||
* @return the ColumnTableFilter that has been set.
|
||||
*/
|
||||
public ColumnBasedTableFilter<ROW_OBJECT> getColumnTableFilter() {
|
||||
return columnTableFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique key that can be used to store preferences for this table.
|
||||
* @return a unique key that can be used to store preferences for this table.
|
||||
*/
|
||||
public String getPreferenceKey() {
|
||||
return uniquePreferenceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the "quick filter" multistate button.
|
||||
* @param filter the filter to add or remove.
|
||||
* @param add if true, the filter is added to the quick list. Otherwise, it is removed.
|
||||
*/
|
||||
public void updateSavedFilters(ColumnBasedTableFilter<ROW_OBJECT> filter, boolean add) {
|
||||
if (add) {
|
||||
ArrayList<ColumnBasedTableFilter<ROW_OBJECT>> list = new ArrayList<>();
|
||||
list.add(filter);
|
||||
list.addAll(savedFilters);
|
||||
savedFilters = list;
|
||||
if (filter.isEquivalent(columnTableFilter)) {
|
||||
setColumnTableFilter(filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
savedFilters.remove(filter);
|
||||
}
|
||||
|
||||
updateColumnFilterButton();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Static Methods
|
||||
//==================================================================================================
|
||||
|
||||
private static String generateFilterPreferenceKey(JTable jTable, String extension) {
|
||||
|
||||
if (jTable instanceof GTable) {
|
||||
|
@ -1039,78 +873,4 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
|||
String clientName = filteredTrace[0].getClassName();
|
||||
return clientName;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private abstract class ColumnFilterActionState
|
||||
extends ActionState<ColumnBasedTableFilter<ROW_OBJECT>> {
|
||||
|
||||
ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super(name, icon, filter);
|
||||
}
|
||||
|
||||
abstract void performAction();
|
||||
}
|
||||
|
||||
String getFilterName(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
String filterName = filter.getName();
|
||||
return filterName == null ? "Unsaved" : filterName;
|
||||
}
|
||||
|
||||
private class ClearFilterActionState extends ColumnFilterActionState {
|
||||
public ClearFilterActionState() {
|
||||
super("Clear Filter", CLEAR_FILTER_ICON, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setColumnTableFilter(null);
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateFilterActionState extends ColumnFilterActionState {
|
||||
public CreateFilterActionState() {
|
||||
super("Create Column Filter", FILTER_OFF_ICON, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
showFilterDialog(textFilterModel);
|
||||
}
|
||||
}
|
||||
|
||||
private class EditFilterActionState extends ColumnFilterActionState {
|
||||
public EditFilterActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
showFilterDialog(textFilterModel);
|
||||
}
|
||||
}
|
||||
|
||||
private class ApplyFilterActionState extends ColumnFilterActionState {
|
||||
public ApplyFilterActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setColumnTableFilter(getUserData());
|
||||
}
|
||||
}
|
||||
|
||||
private class ApplyLastUsedActionState extends ColumnFilterActionState {
|
||||
public ApplyLastUsedActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Apply Last Unsaved", FILTER_ON_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setColumnTableFilter(getUserData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
/* ###
|
||||
* 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 docking.widgets.table.columnfilter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.TableModel;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.menu.*;
|
||||
import docking.widgets.EventTrigger;
|
||||
import docking.widgets.table.GDynamicColumnTableModel;
|
||||
import docking.widgets.table.RowObjectFilterModel;
|
||||
import docking.widgets.table.constraint.dialog.ColumnFilterDialog;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import resources.Icons;
|
||||
import utility.function.Callback;
|
||||
|
||||
/**
|
||||
* A class that manages column filters for a table. This includes creating the UI elements that
|
||||
* allow users to build filters, as well as a means to save and restore filters.
|
||||
*
|
||||
* @param <ROW_OBJECT> the row type
|
||||
*/
|
||||
public class ColumnFilterManager<ROW_OBJECT> {
|
||||
|
||||
public static final String FILTER_EXTENSION = ".FilterExtension";
|
||||
public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield";
|
||||
private static final Icon FILTER_ON_ICON = new GIcon("icon.widget.filterpanel.filter.on");
|
||||
private static final Icon FILTER_OFF_ICON = new GIcon("icon.widget.filterpanel.filter.off");
|
||||
private static final Icon APPLY_FILTER_ICON = Icons.OPEN_FOLDER_ICON;
|
||||
private static final Icon CLEAR_FILTER_ICON = Icons.DELETE_ICON;
|
||||
|
||||
private MultiStateDockingAction<ColumnBasedTableFilter<ROW_OBJECT>> columnFilterAction;
|
||||
private JButton configureButton;
|
||||
private ColumnFilterDialog<ROW_OBJECT> columnFilterDialog;
|
||||
|
||||
private ColumnBasedTableFilter<ROW_OBJECT> lastUsedFilter;
|
||||
private ColumnBasedTableFilter<ROW_OBJECT> currentFilter;
|
||||
private List<ColumnBasedTableFilter<ROW_OBJECT>> savedFilters = new ArrayList<>();
|
||||
|
||||
private JTable table;
|
||||
private RowObjectFilterModel<ROW_OBJECT> rowObjectFilterModel;
|
||||
private String preferenceKey;
|
||||
private Callback filterChangedCallback;
|
||||
|
||||
public ColumnFilterManager(JTable table, RowObjectFilterModel<ROW_OBJECT> rowObjectFilterModel,
|
||||
String preferenceKey, Callback filterChangedCallback) {
|
||||
this.table = Objects.requireNonNull(table);
|
||||
this.rowObjectFilterModel = Objects.requireNonNull(rowObjectFilterModel);
|
||||
this.preferenceKey = Objects.requireNonNull(preferenceKey);
|
||||
this.filterChangedCallback = Objects.requireNonNull(filterChangedCallback);
|
||||
|
||||
configureButton = buildColumnFilterStateButton();
|
||||
|
||||
DockingWindowManager.registerComponentLoadedListener(table,
|
||||
(windowManager, provider) -> initializeSavedFilters());
|
||||
}
|
||||
|
||||
private void initializeSavedFilters() {
|
||||
TableModel model = table.getModel();
|
||||
if (!(model instanceof GDynamicColumnTableModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
GDynamicColumnTableModel<ROW_OBJECT, ?> dynamicModel =
|
||||
(GDynamicColumnTableModel<ROW_OBJECT, ?>) model;
|
||||
|
||||
ColumnFilterSaveManager<ROW_OBJECT> saveManager = new ColumnFilterSaveManager<>(
|
||||
preferenceKey, table, dynamicModel, dynamicModel.getDataSource());
|
||||
|
||||
savedFilters = saveManager.getSavedFilters();
|
||||
Collections.reverse(savedFilters);
|
||||
updateColumnFilterButton();
|
||||
}
|
||||
|
||||
public ColumnBasedTableFilter<ROW_OBJECT> getCurrentFilter() {
|
||||
return currentFilter;
|
||||
}
|
||||
|
||||
public JButton getConfigureButton() {
|
||||
return configureButton;
|
||||
}
|
||||
|
||||
public String getPreferenceKey() {
|
||||
return preferenceKey;
|
||||
}
|
||||
|
||||
public void setFilter(ColumnBasedTableFilter<ROW_OBJECT> newFilter) {
|
||||
if (Objects.equals(newFilter, this.currentFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentFilter != null && !currentFilter.isSaved()) {
|
||||
lastUsedFilter = currentFilter;
|
||||
}
|
||||
currentFilter = newFilter;
|
||||
|
||||
updateColumnFilterButton();
|
||||
if (columnFilterDialog != null) {
|
||||
columnFilterDialog.filterChanged(newFilter);
|
||||
}
|
||||
|
||||
filterChangedCallback.call();
|
||||
}
|
||||
|
||||
public void updateSavedFilters(ColumnBasedTableFilter<ROW_OBJECT> filter, boolean add) {
|
||||
|
||||
if (add) {
|
||||
ArrayList<ColumnBasedTableFilter<ROW_OBJECT>> list = new ArrayList<>();
|
||||
list.add(filter);
|
||||
list.addAll(savedFilters);
|
||||
savedFilters = list;
|
||||
if (filter.isEquivalent(currentFilter)) {
|
||||
setFilter(filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
savedFilters.remove(filter);
|
||||
}
|
||||
|
||||
updateColumnFilterButton();
|
||||
|
||||
filterChangedCallback.call();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (columnFilterDialog != null) {
|
||||
columnFilterDialog.dispose();
|
||||
columnFilterDialog = null;
|
||||
}
|
||||
|
||||
filterChangedCallback = Callback.dummy();
|
||||
}
|
||||
|
||||
private JButton buildColumnFilterStateButton() {
|
||||
|
||||
columnFilterAction =
|
||||
new NonToolbarMultiStateAction<>("Column Filter", "GTableFilterPanel") {
|
||||
|
||||
@Override
|
||||
public void actionStateChanged(
|
||||
ActionState<ColumnBasedTableFilter<ROW_OBJECT>> newActionState,
|
||||
EventTrigger trigger) {
|
||||
if (trigger != EventTrigger.GUI_ACTION) {
|
||||
return;
|
||||
}
|
||||
ColumnFilterActionState state = (ColumnFilterActionState) newActionState;
|
||||
state.performAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed() {
|
||||
showFilterDialog(rowObjectFilterModel);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
HelpLocation helpLocation = new HelpLocation("Trees", "Column_Filters");
|
||||
columnFilterAction.setHelpLocation(helpLocation);
|
||||
|
||||
updateColumnFilterButton();
|
||||
JButton button = columnFilterAction.createButton();
|
||||
DockingWindowManager.getHelpService().registerHelp(button, helpLocation);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void updateColumnFilterButton() {
|
||||
List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> list = getActionStates();
|
||||
columnFilterAction.setActionStates(list);
|
||||
}
|
||||
|
||||
private List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> getActionStates() {
|
||||
List<ActionState<ColumnBasedTableFilter<ROW_OBJECT>>> list = new ArrayList<>();
|
||||
if (currentFilter == null) {
|
||||
list.add(new CreateFilterActionState());
|
||||
}
|
||||
else {
|
||||
list.add(new EditFilterActionState(currentFilter));
|
||||
list.add(new ClearFilterActionState());
|
||||
}
|
||||
if (lastUsedFilter != null) {
|
||||
list.add(new ApplyLastUsedActionState(lastUsedFilter));
|
||||
}
|
||||
for (ColumnBasedTableFilter<ROW_OBJECT> filter : savedFilters) {
|
||||
list.add(new ApplyFilterActionState(filter));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void showFilterDialog(RowObjectFilterModel<ROW_OBJECT> tableModel) {
|
||||
if (columnFilterDialog == null) {
|
||||
if (ColumnFilterDialog.hasFilterableColumns(table, tableModel)) {
|
||||
columnFilterDialog = new ColumnFilterDialog<>(this, table, rowObjectFilterModel);
|
||||
}
|
||||
else {
|
||||
Msg.showError(this, null, "Column Filter Error",
|
||||
"This table contains no filterable columns!");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DockingWindowManager.showDialog(table, columnFilterDialog);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private abstract class ColumnFilterActionState
|
||||
extends ActionState<ColumnBasedTableFilter<ROW_OBJECT>> {
|
||||
|
||||
ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super(name, icon, filter);
|
||||
}
|
||||
|
||||
abstract void performAction();
|
||||
}
|
||||
|
||||
String getFilterName(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
String filterName = filter.getName();
|
||||
return filterName == null ? "Unsaved" : filterName;
|
||||
}
|
||||
|
||||
private class ClearFilterActionState extends ColumnFilterActionState {
|
||||
public ClearFilterActionState() {
|
||||
super("Clear Filter", CLEAR_FILTER_ICON, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setFilter(null);
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateFilterActionState extends ColumnFilterActionState {
|
||||
public CreateFilterActionState() {
|
||||
super("Create Column Filter", FILTER_OFF_ICON, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
showFilterDialog(rowObjectFilterModel);
|
||||
}
|
||||
}
|
||||
|
||||
private class EditFilterActionState extends ColumnFilterActionState {
|
||||
public EditFilterActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
showFilterDialog(rowObjectFilterModel);
|
||||
}
|
||||
}
|
||||
|
||||
private class ApplyFilterActionState extends ColumnFilterActionState {
|
||||
public ApplyFilterActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setFilter(getUserData());
|
||||
}
|
||||
}
|
||||
|
||||
private class ApplyLastUsedActionState extends ColumnFilterActionState {
|
||||
public ApplyLastUsedActionState(ColumnBasedTableFilter<ROW_OBJECT> filter) {
|
||||
super("Apply Last Unsaved", FILTER_ON_ICON, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void performAction() {
|
||||
setFilter(getUserData());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ import javax.swing.JTable;
|
|||
import org.jdom.Element;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.widgets.table.GTableFilterPanel;
|
||||
import docking.widgets.table.RowObjectTableModel;
|
||||
import ghidra.framework.options.PreferenceState;
|
||||
import ghidra.framework.options.SaveState;
|
||||
|
@ -35,7 +34,7 @@ import ghidra.util.Msg;
|
|||
* @param <R> the row type of the table.
|
||||
*/
|
||||
public class ColumnFilterSaveManager<R> {
|
||||
private static final String COLUMN_FILTER_EXTENSION = ".ColumnFilterExtension";
|
||||
|
||||
private static final String COLUMN_FILTER_STATE = "COLUMN_FILTER_STATE";
|
||||
|
||||
private List<ColumnBasedTableFilter<R>> filters = new ArrayList<>();
|
||||
|
@ -46,14 +45,15 @@ public class ColumnFilterSaveManager<R> {
|
|||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param panel The GTableFilterPanel for the table.
|
||||
* @param tablePreferenceKey the key used to save table settings. This is used to make a
|
||||
* preference key for saving the column filters.
|
||||
* @param table The JTable that is filterable.
|
||||
* @param model the TableModel that supports filtering.
|
||||
* @param dataSource the table's DataSource object.
|
||||
*/
|
||||
public ColumnFilterSaveManager(GTableFilterPanel<R> panel, JTable table,
|
||||
public ColumnFilterSaveManager(String tablePreferenceKey, JTable table,
|
||||
RowObjectTableModel<R> model, Object dataSource) {
|
||||
preferenceKey = panel.getPreferenceKey() + COLUMN_FILTER_EXTENSION;
|
||||
preferenceKey = tablePreferenceKey + ColumnFilterManager.FILTER_EXTENSION;
|
||||
loadFromPreferences(table, model, dataSource);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import docking.action.*;
|
|||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.dialogs.InputDialog;
|
||||
import docking.widgets.label.GLabel;
|
||||
import docking.widgets.table.GTableFilterPanel;
|
||||
import docking.widgets.table.RowObjectFilterModel;
|
||||
import docking.widgets.table.columnfilter.*;
|
||||
import docking.widgets.table.constrainteditor.ColumnConstraintEditor;
|
||||
|
@ -50,38 +49,38 @@ import utility.function.Callback;
|
|||
public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||
implements TableFilterDialogModelListener {
|
||||
|
||||
private final ColumnFilterDialogModel<R> filterModel;
|
||||
private ColumnFilterManager<R> filterManager;
|
||||
private ColumnFilterDialogModel<R> dialogModel;
|
||||
|
||||
private JTable table;
|
||||
private RowObjectFilterModel<R> tableModel;
|
||||
|
||||
private JPanel filterPanelContainer;
|
||||
private List<ColumnFilterPanel> filterPanels = new ArrayList<>();
|
||||
|
||||
private Callback closeCallback;
|
||||
private GTableFilterPanel<R> gTableFilterPanel;
|
||||
|
||||
private JPanel bottomPanel;
|
||||
|
||||
private JTable table;
|
||||
private RowObjectFilterModel<R> tableModel;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param gTableFilterPanel the GTableFilterPanel that launched this dialog.
|
||||
*
|
||||
* @param filterManager the filter manager
|
||||
* @param table the table being filtered.
|
||||
* @param tableModel the table model.
|
||||
*/
|
||||
public ColumnFilterDialog(GTableFilterPanel<R> gTableFilterPanel, JTable table,
|
||||
public ColumnFilterDialog(ColumnFilterManager<R> filterManager, JTable table,
|
||||
RowObjectFilterModel<R> tableModel) {
|
||||
super("Table Column Filters", WindowUtilities.areModalDialogsVisible(), true, true, false);
|
||||
this.gTableFilterPanel = gTableFilterPanel;
|
||||
this.filterManager = filterManager;
|
||||
this.table = table;
|
||||
this.tableModel = tableModel;
|
||||
|
||||
ColumnBasedTableFilter<R> columnTableFilter = gTableFilterPanel.getColumnTableFilter();
|
||||
ColumnBasedTableFilter<R> columnTableFilter = filterManager.getCurrentFilter();
|
||||
|
||||
filterModel =
|
||||
dialogModel =
|
||||
new ColumnFilterDialogModel<>(tableModel, table.getColumnModel(), columnTableFilter);
|
||||
filterModel.addListener(this);
|
||||
dialogModel.addListener(this);
|
||||
|
||||
setHelpLocation(new HelpLocation("Trees", "Column_Filters"));
|
||||
addWorkPanel(buildMainPanel());
|
||||
|
@ -97,10 +96,9 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
updateStatus();
|
||||
}
|
||||
|
||||
public static <R> boolean hasFilterableColumns(JTable table,
|
||||
RowObjectFilterModel<R> model) {
|
||||
public static <R> boolean hasFilterableColumns(JTable table, RowObjectFilterModel<R> model) {
|
||||
return !ColumnFilterDialogModel.getAllColumnFilterData(model, table.getColumnModel())
|
||||
.isEmpty();
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
private void addClearFilterButton() {
|
||||
|
@ -119,7 +117,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
DockingAction saveAction = new DockingAction("Save", "Filter") {
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return !filterModel.getFilterRows().isEmpty() && filterModel.isValid();
|
||||
return !dialogModel.getFilterRows().isEmpty() && dialogModel.isValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -140,15 +138,15 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
};
|
||||
loadAction.setDescription("Load Filter");
|
||||
loadAction.setHelpLocation(new HelpLocation("Trees", "Load_Filter"));
|
||||
loadAction.setToolBarData(
|
||||
new ToolBarData(Icons.OPEN_FOLDER_ICON));
|
||||
loadAction.setToolBarData(new ToolBarData(Icons.OPEN_FOLDER_ICON));
|
||||
addAction(loadAction);
|
||||
}
|
||||
|
||||
private void saveFilter() {
|
||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(
|
||||
gTableFilterPanel, table, tableModel, filterModel.getDataSource());
|
||||
ColumnBasedTableFilter<R> filter = filterModel.getTableColumnFilter();
|
||||
String preferenceKey = filterManager.getPreferenceKey();
|
||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
|
||||
table, tableModel, dialogModel.getDataSource());
|
||||
ColumnBasedTableFilter<R> filter = dialogModel.getTableColumnFilter();
|
||||
|
||||
String defaultName = new Date().toString();
|
||||
InputDialog dialog = new InputDialog("Save Filter", "Filter Name: ", defaultName, d -> {
|
||||
|
@ -174,13 +172,14 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
filter.setName(filterName);
|
||||
filterSaveManager.addFilter(filter);
|
||||
filterSaveManager.save();
|
||||
gTableFilterPanel.updateSavedFilters(filter, true);
|
||||
filterModel.setFilter(filter);
|
||||
filterManager.updateSavedFilters(filter, true);
|
||||
dialogModel.setFilter(filter);
|
||||
}
|
||||
|
||||
private void loadFilter() {
|
||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(
|
||||
gTableFilterPanel, table, tableModel, filterModel.getDataSource());
|
||||
String preferenceKey = filterManager.getPreferenceKey();
|
||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
|
||||
table, tableModel, dialogModel.getDataSource());
|
||||
List<ColumnBasedTableFilter<R>> savedFilters = filterSaveManager.getSavedFilters();
|
||||
if (savedFilters.isEmpty()) {
|
||||
Msg.showInfo(this, getComponent(), "No Saved Filters",
|
||||
|
@ -195,7 +194,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
|
||||
ColumnBasedTableFilter<R> selectedFilter = archiveDialog.getSelectedColumnFilter();
|
||||
if (selectedFilter != null) {
|
||||
filterModel.setFilter(selectedFilter);
|
||||
dialogModel.setFilter(selectedFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,14 +222,12 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
bottomPanel = new JPanel(new BorderLayout());
|
||||
JPanel innerPanel = new JPanel(new VerticalLayout(3));
|
||||
|
||||
JButton addAndConditionButton =
|
||||
new JButton("Add AND condition", Icons.ADD_ICON);
|
||||
JButton addAndConditionButton = new JButton("Add AND condition", Icons.ADD_ICON);
|
||||
|
||||
addAndConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.AND));
|
||||
addAndConditionButton.setEnabled(true);
|
||||
|
||||
JButton addOrConditionButton =
|
||||
new JButton("Add OR condition", Icons.ADD_ICON);
|
||||
JButton addOrConditionButton = new JButton("Add OR condition", Icons.ADD_ICON);
|
||||
|
||||
addOrConditionButton.setHorizontalAlignment(SwingConstants.LEFT);
|
||||
addOrConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.OR));
|
||||
|
@ -251,7 +248,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
}
|
||||
sb.append("Column Filter");
|
||||
|
||||
ColumnBasedTableFilter<R> filter = filterModel.getTableColumnFilter();
|
||||
ColumnBasedTableFilter<R> filter = dialogModel.getTableColumnFilter();
|
||||
if (filter != null && filter.getName() != null) {
|
||||
sb.append(": ").append(filter.getName());
|
||||
}
|
||||
|
@ -275,7 +272,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
// * Dialog state is different from applied filter and valid - prompt to apply filter.
|
||||
// * Dialog state is different from applied filter, but invalid - prompt if should really close
|
||||
|
||||
if (!filterModel.hasUnappliedChanges()) {
|
||||
if (!dialogModel.hasUnappliedChanges()) {
|
||||
return true;
|
||||
}
|
||||
if (dialogHasValidFilter()) {
|
||||
|
@ -316,12 +313,12 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
}
|
||||
|
||||
private boolean dialogHasValidFilter() {
|
||||
return filterModel.getTableColumnFilter() != null;
|
||||
return dialogModel.getTableColumnFilter() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dialogClosed() {
|
||||
filterModel.dispose();
|
||||
dialogModel.dispose();
|
||||
if (closeCallback != null) {
|
||||
closeCallback.call();
|
||||
}
|
||||
|
@ -339,28 +336,29 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
}
|
||||
|
||||
private void clearFilter() {
|
||||
this.gTableFilterPanel.setColumnTableFilter(null);
|
||||
filterModel.clear();
|
||||
filterManager.setFilter(null);
|
||||
dialogModel.clear();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void applyFilter() {
|
||||
ColumnBasedTableFilter<R> tableColumnFilter = filterModel.getTableColumnFilter();
|
||||
filterModel.setCurrentlyAppliedFilter(tableColumnFilter);
|
||||
this.gTableFilterPanel.setColumnTableFilter(tableColumnFilter);
|
||||
ColumnBasedTableFilter<R> tableColumnFilter = dialogModel.getTableColumnFilter();
|
||||
dialogModel.setCurrentlyAppliedFilter(tableColumnFilter);
|
||||
filterManager.setFilter(tableColumnFilter);
|
||||
}
|
||||
|
||||
private void loadFilterRows() {
|
||||
filterPanelContainer.removeAll();
|
||||
filterPanels.clear();
|
||||
|
||||
List<DialogFilterRow> filterRows = filterModel.getFilterRows();
|
||||
List<DialogFilterRow> filterRows = dialogModel.getFilterRows();
|
||||
for (int i = 0; i < filterRows.size(); i++) {
|
||||
DialogFilterRow filterRow = filterRows.get(i);
|
||||
ColumnFilterPanel panel = new ColumnFilterPanel(filterRow);
|
||||
if (i != 0) {
|
||||
filterPanelContainer.add(
|
||||
createLogicalOperationLabel(filterRow.getLogicOperation()));
|
||||
LogicOperation op = filterRow.getLogicOperation();
|
||||
GLabel label = createLogicalOperationLabel(op);
|
||||
filterPanelContainer.add(label);
|
||||
}
|
||||
filterPanelContainer.add(panel);
|
||||
filterPanels.add(panel);
|
||||
|
@ -382,14 +380,14 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
headerPanel.add(new GLabel("Filter", SwingConstants.CENTER));
|
||||
headerPanel.add(new GLabel("Filter Value", SwingConstants.CENTER));
|
||||
|
||||
headerPanel.setBorder(new CompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
|
||||
BorderFactory.createEmptyBorder(4, 0, 4, 0)));
|
||||
headerPanel.setBorder(
|
||||
new CompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
|
||||
BorderFactory.createEmptyBorder(4, 0, 4, 0)));
|
||||
return headerPanel;
|
||||
}
|
||||
|
||||
private void addFilterCondition(LogicOperation logicalOperation) {
|
||||
filterModel.createFilterRow(logicalOperation);
|
||||
dialogModel.createFilterRow(logicalOperation);
|
||||
scrollFilterPanelToBottom();
|
||||
}
|
||||
|
||||
|
@ -410,7 +408,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
void updateStatus() {
|
||||
setStatusText(getStatusMessage());
|
||||
|
||||
boolean isValid = filterModel.isValid();
|
||||
boolean isValid = dialogModel.isValid();
|
||||
setOkEnabled(isValid);
|
||||
setApplyEnabled(isValid);
|
||||
|
||||
|
@ -423,11 +421,11 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
}
|
||||
|
||||
public void filterChanged(ColumnBasedTableFilter<R> newFilter) {
|
||||
if (Objects.equals(newFilter, filterModel.getTableColumnFilter())) {
|
||||
if (Objects.equals(newFilter, dialogModel.getTableColumnFilter())) {
|
||||
return;
|
||||
}
|
||||
getComponent().requestFocus(); // work around for java parenting bug where dialog appears behind
|
||||
if (filterModel.hasUnappliedChanges()) {
|
||||
if (dialogModel.hasUnappliedChanges()) {
|
||||
int result = OptionDialog.showYesNoDialog(getComponent(), "Filter Changed",
|
||||
"The filter has been changed externally.\n" +
|
||||
" Do you want to update this editor and lose your current changes?");
|
||||
|
@ -435,14 +433,14 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
return;
|
||||
}
|
||||
}
|
||||
filterModel.setFilter(newFilter);
|
||||
dialogModel.setFilter(newFilter);
|
||||
}
|
||||
|
||||
private String getStatusMessage() {
|
||||
if (filterModel.isEmpty()) {
|
||||
if (dialogModel.isEmpty()) {
|
||||
return "Please add a filter condition!";
|
||||
}
|
||||
if (!filterModel.isValid()) {
|
||||
if (!dialogModel.isValid()) {
|
||||
return "One or more filter values are invalid!";
|
||||
}
|
||||
return "";
|
||||
|
@ -473,8 +471,6 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
|||
}
|
||||
|
||||
void filterRemoved(ColumnBasedTableFilter<R> filter) {
|
||||
gTableFilterPanel.updateSavedFilters(filter, false);
|
||||
|
||||
filterManager.updateSavedFilters(filter, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -193,6 +193,12 @@ public class ConcurrentQBuilder<I, R> {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link ConcurrentQ}.
|
||||
*
|
||||
* @param callback the callback for processing each job
|
||||
* @return the new queue
|
||||
*/
|
||||
public ConcurrentQ<I, R> build(QCallback<I, R> callback) {
|
||||
|
||||
ConcurrentQ<I, R> concurrentQ = new ConcurrentQ<>(callback, getQueue(), getThreadPool(),
|
||||
|
|
|
@ -17,7 +17,7 @@ package ghidra.util;
|
|||
|
||||
/**
|
||||
* Ghidra synchronization lock. This class allows creation of named locks for
|
||||
* synchroniing modification of multiple tables in the Ghidra database.
|
||||
* synchronizing modification of multiple tables in the Ghidra database.
|
||||
*/
|
||||
public class Lock {
|
||||
private Thread owner;
|
||||
|
|
Loading…
Reference in New Issue
Block a user