mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 09:49:23 +02:00
GP-4410 - Version Tracking - Added support for deleting matches; Added table column filters
This commit is contained in:
parent
76977bd514
commit
9f73d23ee4
36 changed files with 1335 additions and 699 deletions
|
@ -26,6 +26,7 @@ project.ext.excludeFromParallelIntegrationTests = true
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":Base")
|
api project(":Base")
|
||||||
|
runtimeOnly project(":CodeCompare")
|
||||||
|
|
||||||
testImplementation project(path: ':Project', configuration: 'testArtifacts')
|
testImplementation project(path: ':Project', configuration: 'testArtifacts')
|
||||||
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
|
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
|
||||||
|
|
|
@ -316,8 +316,55 @@
|
||||||
<P align="left"><A name="Clear_Match"></A>The <b>Clear Match</b> <IMG src="images/undo-apply.png" border="0">
|
<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>
|
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">
|
<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">
|
<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>
|
action will create selections in the source and destination tools for all matches selected in the table.</P>
|
||||||
|
@ -400,6 +447,11 @@
|
||||||
|
|
||||||
<H2><A name="Match_Filters"></A>Match Filters</H2>
|
<H2><A name="Match_Filters"></A>Match Filters</H2>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
|
||||||
|
<H3>Table Filters</H3>
|
||||||
|
|
||||||
|
|
||||||
<BLOCKQUOTE>
|
<BLOCKQUOTE>
|
||||||
<P align="left">The match table has an extensive assortment of filters. There
|
<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:
|
are several commonly used filter controls at the bottom of the table:
|
||||||
|
@ -418,10 +470,17 @@
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
</P>
|
</P>
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
<P>Finally, the <IMG src="images/view-filter.png" border="0"> will show the ancillary filters
|
|
||||||
|
<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
|
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"> .
|
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>
|
Further, the icon may occasionally flash as a reminder that there is a filter applied.</P>
|
||||||
<BR>
|
<BR>
|
||||||
<TABLE border="1" width="90%">
|
<TABLE border="1" width="90%">
|
||||||
|
@ -475,6 +534,16 @@
|
||||||
</TABLE>
|
</TABLE>
|
||||||
</BLOCKQUOTE>
|
</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 -->
|
</BLOCKQUOTE> <!-- end of top-level blockquote -->
|
||||||
|
|
||||||
<P class="providedbyplugin">Provided by: <I>Version Tracking Plugin</I></P>
|
<P class="providedbyplugin">Provided by: <I>Version Tracking Plugin</I></P>
|
||||||
|
|
|
@ -21,8 +21,7 @@ import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import db.*;
|
import db.*;
|
||||||
import ghidra.feature.vt.api.impl.MarkupItemStorage;
|
import ghidra.feature.vt.api.impl.*;
|
||||||
import ghidra.feature.vt.api.impl.VTEvent;
|
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
import ghidra.feature.vt.api.util.VTAssociationStatusException;
|
import ghidra.feature.vt.api.util.VTAssociationStatusException;
|
||||||
import ghidra.framework.data.OpenMode;
|
import ghidra.framework.data.OpenMode;
|
||||||
|
@ -138,33 +137,13 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
||||||
return createMarkupItemDB(markupItemStorage);
|
return createMarkupItemDB(markupItemStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeMarkupItem(MarkupItemStorageDB appliedMarkupItemDB) {
|
// non-interface method; internal API use
|
||||||
|
public void removeStoredMarkupItems(List<MarkupItemImpl> impls) {
|
||||||
|
|
||||||
VTAssociationDB association = (VTAssociationDB) appliedMarkupItemDB.getAssociation();
|
for (MarkupItemImpl impl : impls) {
|
||||||
|
MarkupItemStorage storage = impl.getStorage();
|
||||||
validateAcceptedState(appliedMarkupItemDB, association);
|
if (storage instanceof MarkupItemStorageDB storageDb) {
|
||||||
|
removeMarkupRecord(storageDb.getKey());
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,17 +230,31 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssociation(VTAssociation association) {
|
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 {
|
try {
|
||||||
|
associationDb.removeMarkupItems();
|
||||||
associationTableAdapter.removeAssociaiton(id);
|
associationTableAdapter.removeAssociaiton(id);
|
||||||
session.setChanged(VTEvent.ASSOCIATION_REMOVED, existingAssociation, null);
|
session.setChanged(VTEvent.ASSOCIATION_REMOVED, associationDb, null);
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
session.dbError(e);
|
session.dbError(e);
|
||||||
}
|
}
|
||||||
associationCache.delete(id);
|
associationCache.delete(id);
|
||||||
existingAssociation.setInvalid();
|
associationDb.setInvalid();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,7 +500,7 @@ public class AssociationDatabaseManager implements VTAssociationManager {
|
||||||
throws VTAssociationStatusException {
|
throws VTAssociationStatusException {
|
||||||
if (association.hasAppliedMarkupItems()) {
|
if (association.hasAppliedMarkupItems()) {
|
||||||
throw new VTAssociationStatusException(
|
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);
|
associationHooks.remove(hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeMarkupRecord(DBRecord record) {
|
void removeMarkupRecord(long key) {
|
||||||
try {
|
try {
|
||||||
markupItemTableAdapter.removeMatchMarkupItemRecord(record.getKey());
|
markupItemTableAdapter.removeMarkupItemRecord(key);
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
session.dbError(e);
|
session.dbError(e);
|
||||||
|
|
|
@ -135,7 +135,7 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto
|
||||||
try {
|
try {
|
||||||
MarkupItemStorage storage = new MarkupItemStorageImpl(getAssociation(), getMarkupType(),
|
MarkupItemStorage storage = new MarkupItemStorageImpl(getAssociation(), getMarkupType(),
|
||||||
getSourceAddress(), getDestinationAddress(), getDestinationAddressSource());
|
getSourceAddress(), getDestinationAddress(), getDestinationAddressSource());
|
||||||
associationManager.removeMarkupRecord(record);
|
associationManager.removeMarkupRecord(record.getKey());
|
||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
@ -144,7 +144,8 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MarkupItemStorage setDestinationAddress(Address destinationAddress, String addressSource) {
|
public MarkupItemStorage setDestinationAddress(Address destinationAddress,
|
||||||
|
String addressSource) {
|
||||||
if (destinationAddress == null) {
|
if (destinationAddress == null) {
|
||||||
destinationAddress = Address.NO_ADDRESS;
|
destinationAddress = Address.NO_ADDRESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,4 +296,8 @@ public class VTAssociationDB extends DatabaseObject implements VTAssociation {
|
||||||
public boolean hasAppliedMarkupItems() {
|
public boolean hasAppliedMarkupItems() {
|
||||||
return markupManager.hasAppliedMarkupItems();
|
return markupManager.hasAppliedMarkupItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeMarkupItems() {
|
||||||
|
markupManager.removeMarkupItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ public abstract class VTMatchMarkupItemTableDBAdapter {
|
||||||
|
|
||||||
public abstract RecordIterator getRecords() throws IOException;
|
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;
|
public abstract DBRecord getRecord(long key) throws IOException;
|
||||||
|
|
||||||
|
@ -70,5 +70,6 @@ public abstract class VTMatchMarkupItemTableDBAdapter {
|
||||||
|
|
||||||
public abstract int getRecordCount();
|
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;
|
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.*;
|
||||||
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 java.io.IOException;
|
||||||
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 db.*;
|
||||||
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 ghidra.feature.vt.api.impl.MarkupItemStorage;
|
import ghidra.feature.vt.api.impl.MarkupItemStorage;
|
||||||
import ghidra.feature.vt.api.main.VTSession;
|
import ghidra.feature.vt.api.main.VTSession;
|
||||||
import ghidra.feature.vt.api.markuptype.VTMarkupTypeFactory;
|
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.exception.VersionException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import db.*;
|
|
||||||
|
|
||||||
public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBAdapter {
|
public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBAdapter {
|
||||||
|
|
||||||
private Table table;
|
private Table table;
|
||||||
|
@ -71,20 +64,20 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA
|
||||||
|
|
||||||
record.setLongValue(ASSOCIATION_KEY_COL.column(), association.getKey());
|
record.setLongValue(ASSOCIATION_KEY_COL.column(), association.getKey());
|
||||||
record.setString(ADDRESS_SOURCE_COL.column(), markupItem.getDestinationAddressSource());
|
record.setString(ADDRESS_SOURCE_COL.column(), markupItem.getDestinationAddressSource());
|
||||||
record.setLongValue(SOURCE_ADDRESS_COL.column(), getAddressID(sourceProgram,
|
record.setLongValue(SOURCE_ADDRESS_COL.column(),
|
||||||
markupItem.getSourceAddress()));
|
getAddressID(sourceProgram, markupItem.getSourceAddress()));
|
||||||
|
|
||||||
Address destinationAddress = markupItem.getDestinationAddress();
|
Address destinationAddress = markupItem.getDestinationAddress();
|
||||||
if (destinationAddress != null) {
|
if (destinationAddress != null) {
|
||||||
record.setLongValue(DESTINATION_ADDRESS_COL.column(), getAddressID(destinationProgram,
|
record.setLongValue(DESTINATION_ADDRESS_COL.column(),
|
||||||
markupItem.getDestinationAddress()));
|
getAddressID(destinationProgram, markupItem.getDestinationAddress()));
|
||||||
}
|
}
|
||||||
record.setShortValue(MARKUP_TYPE_COL.column(),
|
record.setShortValue(MARKUP_TYPE_COL.column(),
|
||||||
(short) VTMarkupTypeFactory.getID(markupItem.getMarkupType()));
|
(short) VTMarkupTypeFactory.getID(markupItem.getMarkupType()));
|
||||||
record.setString(SOURCE_VALUE_COL.column(), Stringable.getString(
|
record.setString(SOURCE_VALUE_COL.column(),
|
||||||
markupItem.getSourceValue(), sourceProgram));
|
Stringable.getString(markupItem.getSourceValue(), sourceProgram));
|
||||||
record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(), Stringable.getString(
|
record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(),
|
||||||
markupItem.getDestinationValue(), destinationProgram));
|
Stringable.getString(markupItem.getDestinationValue(), destinationProgram));
|
||||||
record.setByteValue(STATUS_COL.column(), (byte) markupItem.getStatus().ordinal());
|
record.setByteValue(STATUS_COL.column(), (byte) markupItem.getStatus().ordinal());
|
||||||
|
|
||||||
table.putRecord(record);
|
table.putRecord(record);
|
||||||
|
@ -97,7 +90,7 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeMatchMarkupItemRecord(long key) throws IOException {
|
public void removeMarkupItemRecord(long key) throws IOException {
|
||||||
table.deleteRecord(key);
|
table.deleteRecord(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,6 @@ import org.jdom.JDOMException;
|
||||||
import org.jdom.input.SAXBuilder;
|
import org.jdom.input.SAXBuilder;
|
||||||
|
|
||||||
import db.*;
|
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.impl.*;
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
import ghidra.framework.data.OpenMode;
|
import ghidra.framework.data.OpenMode;
|
||||||
|
@ -186,37 +184,47 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean removeMatch(VTMatch match) {
|
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();
|
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);
|
List<VTMatch> matches = session.getMatches(association);
|
||||||
if (matches.size() == 1 && association.getStatus() == VTAssociationStatus.ACCEPTED) {
|
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 sourceAddress = association.getSourceAddress();
|
||||||
Address destinationAddress = association.getDestinationAddress();
|
Address destinationAddress = association.getDestinationAddress();
|
||||||
try {
|
try {
|
||||||
lock.acquire();
|
lock.acquire();
|
||||||
long matchKey = matchDB.getKey();
|
long matchKey = matchDb.getKey();
|
||||||
boolean deleted = matchTableAdapter.deleteRecord(matchKey);
|
boolean deleted = matchTableAdapter.deleteRecord(matchKey);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
matchCache.delete(matchKey);
|
matchCache.delete(matchKey);
|
||||||
|
|
||||||
if (matches.size() == 1) {
|
List<VTMatch> matches = session.getMatches(association);
|
||||||
|
if (matches.isEmpty()) {
|
||||||
// if last match, remove association
|
// 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);
|
DeletedMatch deletedMatch = new DeletedMatch(sourceAddress, destinationAddress);
|
||||||
session.setObjectChanged(VTEvent.MATCH_DELETED, match, deletedMatch, null);
|
session.setObjectChanged(VTEvent.MATCH_DELETED, match, deletedMatch, null);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Match Set " + getID() + " - " + getMatchCount() + " matches [Correlator=" +
|
return "Match Set " + getID() + " - " + getMatchCount() + " matches [Correlator=" +
|
||||||
|
|
|
@ -501,4 +501,8 @@ public class MarkupItemImpl implements VTMarkupItem {
|
||||||
newStatus);
|
newStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// non-interface method
|
||||||
|
public MarkupItemStorage getStorage() {
|
||||||
|
return markupItemStorage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.api.impl;
|
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.db.*;
|
||||||
import ghidra.feature.vt.api.main.VTMarkupItem;
|
import ghidra.feature.vt.api.main.VTMarkupItem;
|
||||||
import ghidra.feature.vt.api.main.VTMarkupItemStatus;
|
import ghidra.feature.vt.api.main.VTMarkupItemStatus;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
import ghidra.util.task.TaskMonitorAdapter;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class MarkupItemManagerImpl {
|
public class MarkupItemManagerImpl {
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ public class MarkupItemManagerImpl {
|
||||||
return Collections.unmodifiableList(markupItems);
|
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> generatedMarkupItems = getGeneratedMarkupItems(monitor);
|
||||||
Collection<VTMarkupItem> databaseMarkupItems = getStoredMarkupItems(monitor);
|
Collection<VTMarkupItem> databaseMarkupItems = getStoredMarkupItems(monitor);
|
||||||
|
@ -104,18 +104,6 @@ public class MarkupItemManagerImpl {
|
||||||
return false;
|
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(
|
private List<VTMarkupItem> replaceGeneratedMarkupItemsWithDBMarkupItems(
|
||||||
Collection<VTMarkupItem> generatedMarkupItems,
|
Collection<VTMarkupItem> generatedMarkupItems,
|
||||||
Collection<VTMarkupItem> databaseMarkupItems) {
|
Collection<VTMarkupItem> databaseMarkupItems) {
|
||||||
|
@ -142,8 +130,45 @@ public class MarkupItemManagerImpl {
|
||||||
markupItem.getSourceAddress().toString(true);
|
markupItem.getSourceAddress().toString(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearCache() {
|
// synchronized due to write of 'markupItems'
|
||||||
|
public synchronized void clearCache() {
|
||||||
markupItems = EMPTY_LIST;
|
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;
|
package ghidra.feature.vt.api.impl;
|
||||||
|
|
||||||
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
|
import java.util.*;
|
||||||
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class MatchSetImpl implements VTMatchSet {
|
public class MatchSetImpl implements VTMatchSet {
|
||||||
private ProgramCorrelatorInfoFake correlatorInfo;
|
private ProgramCorrelatorInfoFake correlatorInfo;
|
||||||
private final VTSession session;
|
private final VTSession session;
|
||||||
|
@ -77,11 +75,8 @@ public class MatchSetImpl implements VTMatchSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasRemovableMatches() {
|
public void deleteMatch(VTMatch match) {
|
||||||
VTProgramCorrelatorInfo info = getProgramCorrelatorInfo();
|
throw new UnsupportedOperationException();
|
||||||
String correlatorClassName = info.getCorrelatorClassName();
|
|
||||||
return correlatorClassName.equals(ManualMatchProgramCorrelator.class.getName()) ||
|
|
||||||
correlatorClassName.equals(ImpliedMatchProgramCorrelator.class.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* ###
|
/* ###
|
||||||
* IP: GHIDRA
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,11 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.api.main;
|
package ghidra.feature.vt.api.main;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import ghidra.feature.vt.api.impl.VTProgramCorrelatorInfo;
|
import ghidra.feature.vt.api.impl.VTProgramCorrelatorInfo;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for all the matches generated from a single program correlator run.
|
* 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.
|
* Returns the number of matches contained in this match set.
|
||||||
* @return
|
* @return the number of matches contained in this match set.
|
||||||
*/
|
*/
|
||||||
public int getMatchCount();
|
public int getMatchCount();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a unique id for this match set. The ids are one-up numbers indicating the order this
|
* 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.
|
* match set was generated in relation to other match sets in the VTSession.
|
||||||
* @return
|
* @return the id
|
||||||
*/
|
*/
|
||||||
public int getID();
|
public int getID();
|
||||||
|
|
||||||
|
@ -72,7 +71,7 @@ public interface VTMatchSet {
|
||||||
* Returns a collection of all matches for the given association.
|
* Returns a collection of all matches for the given association.
|
||||||
* @param association the association for which to search for matches.
|
* @param association the association for which to search for matches.
|
||||||
* @return a collection of all matches for the given association.
|
* @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);
|
public Collection<VTMatch> getMatches(VTAssociation association);
|
||||||
|
|
||||||
|
@ -90,16 +89,51 @@ public interface VTMatchSet {
|
||||||
public Collection<VTMatch> getMatches(Address sourceAddress, Address destinationAddress);
|
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
|
* Deletes the given match from this match set.
|
||||||
* match sets "Manual Matches" and "Implied Matches".
|
* <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.
|
* @param match the match to remove.
|
||||||
* @return true if the match was removed.
|
* @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);
|
public boolean removeMatch(VTMatch match);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this match set supports removing matches.
|
* Returns true
|
||||||
* @return true if this match set supports removing matches.
|
* @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);
|
super("Remove", VTPlugin.OWNER);
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
|
|
||||||
// setToolBarData(new ToolBarData(ICON, MENU_GROUP));
|
|
||||||
setPopupMenuData(new MenuData(new String[] { "Remove Match" }, ICON, MENU_GROUP));
|
setPopupMenuData(new MenuData(new String[] { "Remove Match" }, ICON, MENU_GROUP));
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
|
setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
|
||||||
|
@ -64,17 +63,7 @@ public class RemoveMatchAction extends DockingAction {
|
||||||
}
|
}
|
||||||
VTMatchContext matchContext = (VTMatchContext) context;
|
VTMatchContext matchContext = (VTMatchContext) context;
|
||||||
List<VTMatch> matches = matchContext.getSelectedMatches();
|
List<VTMatch> matches = matchContext.getSelectedMatches();
|
||||||
if (matches.size() == 0) {
|
return !matches.isEmpty();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -282,12 +282,6 @@ public abstract class AbstractAddressRangeFilter<T> extends AncillaryFilter<T>
|
||||||
fireStatusChanged(getFilterStatus());
|
fireStatusChanged(getFilterStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
lowerAddressRangeTextField.setText(MIN_ADDRESS_VALUE.toString());
|
|
||||||
upperAddressRangeTextField.setText(MAX_ADDRESS_VALUE.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JComponent getComponent() {
|
public JComponent getComponent() {
|
||||||
return component;
|
return component;
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.gui.filters;
|
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.*;
|
||||||
import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.NONE;
|
|
||||||
|
|
||||||
import java.awt.Container;
|
import java.awt.Container;
|
||||||
import java.awt.LayoutManager;
|
import java.awt.LayoutManager;
|
||||||
|
@ -151,13 +150,6 @@ public abstract class CheckBoxBasedAncillaryFilter<T> extends AncillaryFilter<T>
|
||||||
return FilterShortcutState.REQUIRES_CHECK;
|
return FilterShortcutState.REQUIRES_CHECK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
for (CheckBoxInfo<T> info : checkBoxInfos) {
|
|
||||||
info.setSelected(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FilterState getFilterState() {
|
public FilterState getFilterState() {
|
||||||
FilterState state = new FilterState(this);
|
FilterState state = new FilterState(this);
|
||||||
|
|
|
@ -46,8 +46,6 @@ public abstract class Filter<T> {
|
||||||
|
|
||||||
public abstract FilterEditingStatus getFilterStatus();
|
public abstract FilterEditingStatus getFilterStatus();
|
||||||
|
|
||||||
public abstract void clearFilter();
|
|
||||||
|
|
||||||
public abstract JComponent getComponent();
|
public abstract JComponent getComponent();
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* ###
|
/* ###
|
||||||
* IP: GHIDRA
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 interface FilterDialogModel<T> {
|
||||||
|
|
||||||
public void addFilter( Filter<T> filter );
|
public void addFilter(Filter<T> filter);
|
||||||
|
|
||||||
public void forceRefilter();
|
public void forceRefilter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will be called when the visibility of the dialog using this model has changed
|
* Will be called when the visibility of the dialog using this model has changed
|
||||||
|
* @param isVisible true if visible
|
||||||
*/
|
*/
|
||||||
public void dialogVisibilityChanged( boolean isVisible );
|
public void dialogVisibilityChanged(boolean isVisible);
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,12 +243,6 @@ public class TagFilter extends AncillaryFilter<VTMatch> {
|
||||||
excludedTags = getTagsFromText(tagText);
|
excludedTags = getTagsFromText(tagText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
excludedTags.clear();
|
|
||||||
excludedTagsLabel.setText(ALL_TAGS_INCLUDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JComponent getComponent() {
|
public JComponent getComponent() {
|
||||||
return component;
|
return component;
|
||||||
|
|
|
@ -150,13 +150,14 @@ public class VTPlugin extends Plugin {
|
||||||
|
|
||||||
private void addCustomPlugins() {
|
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();
|
List<Plugin> plugins = tool.getManagedPlugins();
|
||||||
Set<String> existingNames =
|
Set<String> existingNames =
|
||||||
plugins.stream().map(c -> c.getName()).collect(Collectors.toSet());
|
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.
|
// 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
|
// 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
|
// easier than modifying the default to file to load the plugins, since the amount of xml
|
||||||
// required for that is non-trivial.
|
// required for that is non-trivial.
|
||||||
|
|
|
@ -127,12 +127,6 @@ public abstract class AbstractDoubleRangeFilter<T> extends Filter<T>
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
lowerBoundField.setText(minValue.toString());
|
|
||||||
upperBoundField.setText(maxValue.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FilterEditingStatus getFilterStatus() {
|
public FilterEditingStatus getFilterStatus() {
|
||||||
FilterEditingStatus lowerStatus = lowerBoundField.getFilterStatus();
|
FilterEditingStatus lowerStatus = lowerBoundField.getFilterStatus();
|
||||||
|
|
|
@ -89,11 +89,6 @@ public class LengthFilter extends Filter<VTMatch> {
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
textField.setText(DEFAULT_FILTER_VALUE.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FilterEditingStatus getFilterStatus() {
|
public FilterEditingStatus getFilterStatus() {
|
||||||
return textField.getFilterStatus();
|
return textField.getFilterStatus();
|
||||||
|
|
|
@ -34,6 +34,8 @@ import javax.swing.table.*;
|
||||||
import docking.*;
|
import docking.*;
|
||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
import docking.widgets.table.*;
|
import docking.widgets.table.*;
|
||||||
|
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
||||||
|
import docking.widgets.table.columnfilter.ColumnFilterManager;
|
||||||
import docking.widgets.table.threaded.ThreadedTableModel;
|
import docking.widgets.table.threaded.ThreadedTableModel;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.services.FunctionComparisonService;
|
import ghidra.app.services.FunctionComparisonService;
|
||||||
|
@ -80,6 +82,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
||||||
|
|
||||||
private AncillaryFilterDialogComponentProvider<VTMatch> ancillaryFilterDialog;
|
private AncillaryFilterDialogComponentProvider<VTMatch> ancillaryFilterDialog;
|
||||||
private JButton ancillaryFilterButton;
|
private JButton ancillaryFilterButton;
|
||||||
|
private ColumnFilterManager<VTMatch> columnFilterManager;
|
||||||
|
private VTColumnFilter vtColumnFilter;
|
||||||
|
|
||||||
private FilterIconFlashTimer<VTMatch> iconTimer;
|
private FilterIconFlashTimer<VTMatch> iconTimer;
|
||||||
private Set<Filter<VTMatch>> filters = new HashSet<>();
|
private Set<Filter<VTMatch>> filters = new HashSet<>();
|
||||||
|
@ -386,8 +390,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
||||||
JPanel innerPanel = new JPanel(new HorizontalLayout(4));
|
JPanel innerPanel = new JPanel(new HorizontalLayout(4));
|
||||||
innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
|
innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
|
||||||
|
|
||||||
JComponent nameFilterPanel = createTextFilterPanel();
|
JComponent textFilterPanel = createTextFilterPanel();
|
||||||
parentPanel.add(nameFilterPanel, BorderLayout.CENTER);
|
parentPanel.add(textFilterPanel, BorderLayout.CENTER);
|
||||||
parentPanel.add(innerPanel, BorderLayout.EAST);
|
parentPanel.add(innerPanel, BorderLayout.EAST);
|
||||||
|
|
||||||
JComponent scoreFilterPanel = createScoreFilterPanel();
|
JComponent scoreFilterPanel = createScoreFilterPanel();
|
||||||
|
@ -409,13 +413,33 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
||||||
helpService.registerHelp(parentPanel, filterHelpLocation);
|
helpService.registerHelp(parentPanel, filterHelpLocation);
|
||||||
helpService.registerHelp(ancillaryFilterButton, filterHelpLocation);
|
helpService.registerHelp(ancillaryFilterButton, filterHelpLocation);
|
||||||
|
|
||||||
|
JButton columnFilterButton = createColumnFilterButton();
|
||||||
|
innerPanel.add(columnFilterButton);
|
||||||
|
|
||||||
innerPanel.add(ancillaryFilterButton);
|
innerPanel.add(ancillaryFilterButton);
|
||||||
|
|
||||||
return parentPanel;
|
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() {
|
private JComponent createTextFilterPanel() {
|
||||||
// MatchNameFilter nameFilterPanel = new MatchNameFilter(controller, matchesTable);
|
|
||||||
AllTextFilter<VTMatch> allTextFilter =
|
AllTextFilter<VTMatch> allTextFilter =
|
||||||
new AllTextFilter<>(controller, matchesTable, matchesTableModel);
|
new AllTextFilter<>(controller, matchesTable, matchesTableModel);
|
||||||
allTextFilter.setName(TEXT_FILTER_NAME);
|
allTextFilter.setName(TEXT_FILTER_NAME);
|
||||||
|
@ -506,6 +530,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
ancillaryFilterDialog.dispose();
|
ancillaryFilterDialog.dispose();
|
||||||
|
|
||||||
|
columnFilterManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1069,4 +1095,73 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
|
||||||
return list;
|
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;
|
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.db.VTMatchSetDB;
|
||||||
import ghidra.feature.vt.api.main.VTMatch;
|
import ghidra.feature.vt.api.main.VTMatch;
|
||||||
import ghidra.feature.vt.api.main.VTSession;
|
import ghidra.feature.vt.api.main.VTSession;
|
||||||
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
@ -38,26 +40,75 @@ public class RemoveMatchTask extends VtTask {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean removeMatches(TaskMonitor monitor) throws CancelledException {
|
private void removeMatches(TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
monitor.setMessage("Removing matches");
|
monitor.setMessage("Removing matches");
|
||||||
monitor.initialize(matches.size());
|
int n = matches.size();
|
||||||
boolean failed = false;
|
monitor.initialize(n);
|
||||||
for (VTMatch match : matches) {
|
|
||||||
|
//
|
||||||
|
// 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();
|
monitor.checkCancelled();
|
||||||
|
VTMatch match = it.next();
|
||||||
VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
|
VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
|
||||||
boolean matchRemoved = matchSet.removeMatch(match);
|
if (matchSet.removeMatch(match)) {
|
||||||
if (!matchRemoved) {
|
it.remove();
|
||||||
failed = true;
|
|
||||||
}
|
}
|
||||||
monitor.incrementProgress(1);
|
monitor.incrementProgress(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.setProgress(matches.size());
|
if (list.isEmpty()) {
|
||||||
if (failed) {
|
return;
|
||||||
reportError("One or more of your matches could not be removed." +
|
|
||||||
"\nNote: You can't remove a match if it is currently accepted.");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearFilter() {
|
|
||||||
textField.setText(defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FilterEditingStatus getFilterStatus() {
|
public FilterEditingStatus getFilterStatus() {
|
||||||
return textField.getFilterStatus();
|
return textField.getFilterStatus();
|
||||||
|
|
|
@ -115,7 +115,7 @@ public class ImpliedMatchUtils {
|
||||||
VTMatchSet impliedMatchSet = session.getImpliedMatchSet();
|
VTMatchSet impliedMatchSet = session.getImpliedMatchSet();
|
||||||
for (VTMatch vtMatch : matches) {
|
for (VTMatch vtMatch : matches) {
|
||||||
if (vtMatch.getMatchSet() == impliedMatchSet) {
|
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
|
* Method for finding version tracking implied matches given an accepted matched
|
||||||
* function. Each referenced data and function that exist in equivalent sections
|
* function. Each referenced data and function that exist in equivalent sections
|
||||||
* of the matched source and destination functions will added to the current
|
* of the matched source and destination functions will be returned in the given set.
|
||||||
* version tracking session as an implied match.
|
|
||||||
*
|
*
|
||||||
* @param sourceFunction The matched function from the source program
|
* @param sourceFunction The matched function from the source program
|
||||||
* @param destinationFunction The matched function from the destination 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 ghidra.feature.vt.db.VTTestUtils.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.junit.*;
|
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.api.main.VTMatch;
|
||||||
import ghidra.feature.vt.gui.plugin.*;
|
import ghidra.feature.vt.gui.plugin.*;
|
||||||
import ghidra.feature.vt.gui.task.AcceptMatchTask;
|
import ghidra.feature.vt.gui.task.AcceptMatchTask;
|
||||||
|
import ghidra.feature.vt.gui.task.VtTask;
|
||||||
import ghidra.feature.vt.gui.util.VTOptionDefines;
|
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.options.Options;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.program.database.ProgramDB;
|
import ghidra.program.database.ProgramDB;
|
||||||
|
@ -39,10 +38,7 @@ import ghidra.program.model.listing.*;
|
||||||
import ghidra.program.model.symbol.*;
|
import ghidra.program.model.symbol.*;
|
||||||
import ghidra.program.model.util.CodeUnitInsertionException;
|
import ghidra.program.model.util.CodeUnitInsertionException;
|
||||||
import ghidra.test.*;
|
import ghidra.test.*;
|
||||||
import ghidra.util.exception.CancelledException;
|
|
||||||
import ghidra.util.exception.InvalidInputException;
|
import ghidra.util.exception.InvalidInputException;
|
||||||
import ghidra.util.task.Task;
|
|
||||||
import ghidra.util.task.TaskMonitor;
|
|
||||||
|
|
||||||
public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
|
||||||
|
@ -53,13 +49,8 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
private ProgramDB sourceProgram;
|
private ProgramDB sourceProgram;
|
||||||
private ProgramDB destinationProgram;
|
private ProgramDB destinationProgram;
|
||||||
private VTPlugin plugin;
|
private VTPlugin plugin;
|
||||||
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
|
|
||||||
private Options options;
|
private Options options;
|
||||||
|
|
||||||
public VTMatchAcceptTest() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
|
||||||
|
@ -70,7 +61,6 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
|
||||||
ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder();
|
ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder();
|
||||||
destinationProgram = destinationBuilder.getProgram();
|
destinationProgram = destinationBuilder.getProgram();
|
||||||
destinationProgram.addListener(eventRecorder);
|
|
||||||
|
|
||||||
tool = env.getTool();
|
tool = env.getTool();
|
||||||
|
|
||||||
|
@ -84,29 +74,21 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||||
|
|
||||||
options = controller.getOptions();
|
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
|
@After
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
waitForBusyTool(tool);
|
|
||||||
destinationProgram.flushEvents();
|
|
||||||
waitForSwing();
|
|
||||||
|
|
||||||
env.dispose();
|
env.dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAcceptWithApplyDataLabels() throws Exception {
|
public void testAcceptWithApplyDataLabels() throws Exception {
|
||||||
|
|
||||||
//
|
//
|
||||||
// BTW this test exposes a bug because the hook that runs when you apply data on accept was.
|
// 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
|
// exhibiting a side effect, causing the destination address to be set. When the hook was
|
||||||
// not set the destination address, the accept task was not setting the destination address
|
// changed to not set the destination address, the accept task was not setting the
|
||||||
// as it should.
|
// destination address as it should.
|
||||||
//
|
//
|
||||||
|
|
||||||
options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true);
|
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 {
|
private void runTask(VtTask task) {
|
||||||
|
controller.runVTTask(task);
|
||||||
task.run(TaskMonitor.DUMMY);
|
waitForProgram(destinationProgram);
|
||||||
destinationProgram.flushEvents();
|
|
||||||
waitForSwing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Data setData(DataType dataType, int dtLength, Address address, Program program)
|
private Data setData(DataType dataType, int dtLength, Address address, Program program)
|
||||||
|
@ -170,14 +150,4 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
}
|
}
|
||||||
return data;
|
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 ProgramDB destinationProgram;
|
||||||
private VTPlugin plugin;
|
private VTPlugin plugin;
|
||||||
|
|
||||||
// TODO: debug
|
|
||||||
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
|
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
|
||||||
|
|
||||||
@Before
|
@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 {
|
public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
|
||||||
|
|
||||||
|
private String name = "DummyTestProgramCorrelator";
|
||||||
private int matchCount = 1;
|
private int matchCount = 1;
|
||||||
|
|
||||||
public DummyTestProgramCorrelator() {
|
public DummyTestProgramCorrelator() {
|
||||||
|
@ -84,8 +85,12 @@ public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "DummyTestProgramCorrelator";
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
||||||
assertEquals(VTEvent.MATCH_ADDED, events.get(0).getEventType());
|
assertEquals(VTEvent.MATCH_ADDED, events.get(0).getEventType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("removal") // ignore the warning until removeMatch() is removed
|
||||||
@Test
|
@Test
|
||||||
public void testEventsForRemovingLastMatchForAssociation() {
|
public void testEventsForRemovingLastMatchForAssociation() {
|
||||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||||
|
@ -134,6 +135,22 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
||||||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
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
|
@Test
|
||||||
public void testEventsForRemovingNonLastMatchForAssociation() {
|
public void testEventsForRemovingNonLastMatchForAssociation() {
|
||||||
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
VTMatchSet manualMatchSet = db.getManualMatchSet();
|
||||||
|
@ -149,6 +166,21 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
|
||||||
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
|
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
|
@Test
|
||||||
public void testEventsForRejectingMatch() throws VTAssociationStatusException {
|
public void testEventsForRejectingMatch() throws VTAssociationStatusException {
|
||||||
VTMatchSet matchSet = createMatchSet();
|
VTMatchSet matchSet = createMatchSet();
|
||||||
|
|
|
@ -24,29 +24,21 @@ import javax.swing.*;
|
||||||
import javax.swing.border.BevelBorder;
|
import javax.swing.border.BevelBorder;
|
||||||
import javax.swing.event.*;
|
import javax.swing.event.*;
|
||||||
import javax.swing.table.TableColumnModel;
|
import javax.swing.table.TableColumnModel;
|
||||||
import javax.swing.table.TableModel;
|
|
||||||
|
|
||||||
import org.jdom.Element;
|
import org.jdom.Element;
|
||||||
|
|
||||||
import docking.DockingWindowManager;
|
import docking.DockingWindowManager;
|
||||||
import docking.menu.*;
|
|
||||||
import docking.widgets.EmptyBorderButton;
|
import docking.widgets.EmptyBorderButton;
|
||||||
import docking.widgets.EventTrigger;
|
|
||||||
import docking.widgets.filter.*;
|
import docking.widgets.filter.*;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
||||||
import docking.widgets.table.columnfilter.ColumnFilterSaveManager;
|
import docking.widgets.table.columnfilter.ColumnFilterManager;
|
||||||
import docking.widgets.table.constraint.dialog.ColumnFilterDialog;
|
|
||||||
import generic.theme.GIcon;
|
|
||||||
import ghidra.framework.options.PreferenceState;
|
import ghidra.framework.options.PreferenceState;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
|
||||||
import ghidra.util.datastruct.WeakSet;
|
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
import ghidra.util.task.SwingUpdateManager;
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
import help.HelpService;
|
import help.HelpService;
|
||||||
import resources.Icons;
|
|
||||||
import utilities.util.reflection.ReflectionUtilities;
|
import utilities.util.reflection.ReflectionUtilities;
|
||||||
import utility.function.Callback;
|
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";
|
public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield";
|
||||||
private static final String FILTER_STATE = "FILTER_STATE";
|
private static final String FILTER_STATE = "FILTER_STATE";
|
||||||
private static final String FILTER_EXTENSION = ".FilterExtension";
|
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 JTable table;
|
||||||
private RowObjectFilterModel<ROW_OBJECT> textFilterModel;
|
private RowObjectFilterModel<ROW_OBJECT> rowObjectFilterModel;
|
||||||
private JLabel searchLabel;
|
private JLabel searchLabel;
|
||||||
|
|
||||||
private FilterTextField filterField;
|
private FilterTextField filterField;
|
||||||
private FilterListener filterListener = new GTableFilterListener();
|
private FilterListener filterListener = new GTableFilterListener();
|
||||||
|
|
||||||
private WeakSet<Callback> listeners =
|
|
||||||
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
|
|
||||||
|
|
||||||
private FilterOptions filterOptions = new FilterOptions();
|
private FilterOptions filterOptions = new FilterOptions();
|
||||||
private TableTextFilterFactory<ROW_OBJECT> filterFactory =
|
private TableTextFilterFactory<ROW_OBJECT> filterFactory =
|
||||||
new DefaultTableTextFilterFactory<>(filterOptions);
|
new DefaultTableTextFilterFactory<>(filterOptions);
|
||||||
private RowFilterTransformer<ROW_OBJECT> transformer;
|
private RowFilterTransformer<ROW_OBJECT> transformer;
|
||||||
private TableFilter<ROW_OBJECT> secondaryTableFilter;
|
private TableFilter<ROW_OBJECT> secondaryTableFilter;
|
||||||
private ColumnBasedTableFilter<ROW_OBJECT> columnTableFilter;
|
|
||||||
private List<ColumnBasedTableFilter<ROW_OBJECT>> savedFilters = new ArrayList<>();
|
|
||||||
private EmptyBorderButton filterStateButton;
|
private EmptyBorderButton filterStateButton;
|
||||||
|
|
||||||
|
private ColumnFilterManager<ROW_OBJECT> columnFilterManager;
|
||||||
|
|
||||||
private String uniquePreferenceKey;
|
private String uniquePreferenceKey;
|
||||||
|
|
||||||
private MultiStateDockingAction<ColumnBasedTableFilter<ROW_OBJECT>> columnFilterAction;
|
private SwingUpdateManager filterUpdater = new SwingUpdateManager(250, 1000, () -> {
|
||||||
private ColumnFilterDialog<ROW_OBJECT> columnFilterDialog;
|
|
||||||
private ColumnBasedTableFilter<ROW_OBJECT> lastUsedColumnFilter;
|
|
||||||
|
|
||||||
private SwingUpdateManager updateManager = new SwingUpdateManager(250, 1000, () -> {
|
|
||||||
String text = filterField.getText();
|
String text = filterField.getText();
|
||||||
TableFilter<ROW_OBJECT> tableFilter = filterFactory.getTableFilter(text, transformer);
|
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.
|
// result of a filter, the table does not know this and may update the wrong row data.
|
||||||
table.editingCanceled(null);
|
table.editingCanceled(null);
|
||||||
|
|
||||||
textFilterModel.setTableFilter(
|
ColumnBasedTableFilter<ROW_OBJECT> columnFilter = columnFilterManager.getCurrentFilter();
|
||||||
getCombinedTableFilter(secondaryTableFilter, tableFilter, columnTableFilter));
|
rowObjectFilterModel.setTableFilter(
|
||||||
|
getCombinedTableFilter(secondaryTableFilter, tableFilter, columnFilter));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** I'm a field so that my weak reference won't go away */
|
/** 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
|
@Override
|
||||||
public void columnRemoved(TableColumnModelEvent e) {
|
public void columnRemoved(TableColumnModelEvent e) {
|
||||||
updateTableContents();
|
filterUpdater.updateLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void columnAdded(TableColumnModelEvent e) {
|
public void columnAdded(TableColumnModelEvent e) {
|
||||||
updateTableContents();
|
filterUpdater.updateLater();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -231,13 +213,16 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
String filterLabel) {
|
String filterLabel) {
|
||||||
this.table = table;
|
this.table = table;
|
||||||
|
|
||||||
buildPanel(filterLabel);
|
|
||||||
|
|
||||||
uniquePreferenceKey = createUniqueFilterPreferenceKey(table);
|
uniquePreferenceKey = createUniqueFilterPreferenceKey(table);
|
||||||
|
|
||||||
transformer = new DefaultRowFilterTransformer<>(tableModel, table.getColumnModel());
|
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();
|
TableColumnModel columnModel = table.getColumnModel();
|
||||||
columnModel.addColumnModelListener(columnModelListener);
|
columnModel.addColumnModelListener(columnModelListener);
|
||||||
|
@ -246,12 +231,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
table.addPropertyChangeListener(badProgrammingPropertyChangeListener);
|
table.addPropertyChangeListener(badProgrammingPropertyChangeListener);
|
||||||
|
|
||||||
DockingWindowManager.registerComponentLoadedListener(this,
|
DockingWindowManager.registerComponentLoadedListener(this,
|
||||||
(windowManager, provider) -> initialize(windowManager));
|
(windowManager, provider) -> loadFilterPreference(windowManager));
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize(DockingWindowManager windowManager) {
|
|
||||||
loadFilterPreference(windowManager);
|
|
||||||
initializeSavedFilters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFilterPreference(DockingWindowManager dockingWindowManager) {
|
private void loadFilterPreference(DockingWindowManager dockingWindowManager) {
|
||||||
|
@ -283,7 +263,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
if (xmlElement != null) {
|
if (xmlElement != null) {
|
||||||
this.filterOptions = FilterOptions.restoreFromXML(xmlElement);
|
this.filterOptions = FilterOptions.restoreFromXML(xmlElement);
|
||||||
updateFilterFactory();
|
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.
|
* @param newFilter the ColumnTableFilter to use for filtering this table.
|
||||||
*/
|
*/
|
||||||
public void setColumnTableFilter(ColumnBasedTableFilter<ROW_OBJECT> newFilter) {
|
public void setColumnTableFilter(ColumnBasedTableFilter<ROW_OBJECT> newFilter) {
|
||||||
if (Objects.equals(newFilter, this.columnTableFilter)) {
|
columnFilterManager.setFilter(newFilter);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (columnTableFilter != null && !columnTableFilter.isSaved()) {
|
|
||||||
lastUsedColumnFilter = columnTableFilter;
|
|
||||||
}
|
|
||||||
columnTableFilter = newFilter;
|
|
||||||
updateTableContents();
|
|
||||||
updateColumnFilterButton();
|
|
||||||
if (columnFilterDialog != null) {
|
|
||||||
columnFilterDialog.filterChanged(newFilter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -351,7 +320,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
*/
|
*/
|
||||||
public void setFilterRowTransformer(RowFilterTransformer<ROW_OBJECT> transformer) {
|
public void setFilterRowTransformer(RowFilterTransformer<ROW_OBJECT> transformer) {
|
||||||
this.transformer = 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) {
|
public void setSecondaryFilter(TableFilter<ROW_OBJECT> tableFilter) {
|
||||||
this.secondaryTableFilter = tableFilter;
|
this.secondaryTableFilter = tableFilter;
|
||||||
updateTableContents();
|
filterUpdater.updateLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -373,7 +342,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
public void setFilterOptions(FilterOptions filterOptions) {
|
public void setFilterOptions(FilterOptions filterOptions) {
|
||||||
this.filterOptions = filterOptions;
|
this.filterOptions = filterOptions;
|
||||||
updateFilterFactory();
|
updateFilterFactory();
|
||||||
updateTableContents();
|
filterUpdater.updateLater();
|
||||||
doSaveState();
|
doSaveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +363,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
add(buildFilterStateButton());
|
add(buildFilterStateButton());
|
||||||
if (isTableColumnFilterableModel()) {
|
if (isTableColumnFilterableModel()) {
|
||||||
add(Box.createHorizontalStrut(5));
|
add(Box.createHorizontalStrut(5));
|
||||||
add(buildColumnFilterStateButton());
|
add(columnFilterManager.getConfigureButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
HelpService helpService = DockingWindowManager.getHelpService();
|
HelpService helpService = DockingWindowManager.getHelpService();
|
||||||
|
@ -425,107 +394,6 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
return table.getModel() instanceof RowObjectFilterModel;
|
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() {
|
private void updateFilterFactory() {
|
||||||
filterStateButton.setIcon(filterOptions.getFilterStateIcon());
|
filterStateButton.setIcon(filterOptions.getFilterStateIcon());
|
||||||
filterStateButton.setToolTipText(filterOptions.getFilterDescription());
|
filterStateButton.setToolTipText(filterOptions.getFilterDescription());
|
||||||
|
@ -587,17 +455,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RowObjectFilterModel<ROW_OBJECT> getTableFilterModel() {
|
public RowObjectFilterModel<ROW_OBJECT> getTableFilterModel() {
|
||||||
return textFilterModel;
|
return rowObjectFilterModel;
|
||||||
}
|
|
||||||
|
|
||||||
/** Convenience method to refilter the table's contents */
|
|
||||||
private void updateTableContents() {
|
|
||||||
updateManager.updateLater();
|
|
||||||
notifyFilterChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyFilterChanged() {
|
|
||||||
listeners.forEach(callback -> callback.call());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
@ -609,13 +467,11 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
columnModel.removeColumnModelListener(columnModelListener);
|
columnModel.removeColumnModelListener(columnModelListener);
|
||||||
columnModelListener = null;
|
columnModelListener = null;
|
||||||
|
|
||||||
if (columnFilterDialog != null) {
|
columnFilterManager.dispose();
|
||||||
columnFilterDialog.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
table.removePropertyChangeListener(badProgrammingPropertyChangeListener);
|
table.removePropertyChangeListener(badProgrammingPropertyChangeListener);
|
||||||
|
|
||||||
updateManager.dispose();
|
filterUpdater.dispose();
|
||||||
if (table instanceof GTable) {
|
if (table instanceof GTable) {
|
||||||
((GTable) table).dispose();
|
((GTable) table).dispose();
|
||||||
}
|
}
|
||||||
|
@ -696,7 +552,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
return viewRow;
|
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.
|
* @return the row in the table for the given model row.
|
||||||
*/
|
*/
|
||||||
public int getViewRow(int modelRow) {
|
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
|
* @return the row object matching the given index
|
||||||
*/
|
*/
|
||||||
public ROW_OBJECT getRowObject(int viewRow) {
|
public ROW_OBJECT getRowObject(int viewRow) {
|
||||||
ROW_OBJECT rowObject = textFilterModel.getRowObject(viewRow);
|
ROW_OBJECT rowObject = rowObjectFilterModel.getRowObject(viewRow);
|
||||||
return rowObject;
|
return rowObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -736,7 +592,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int viewRow = textFilterModel.getViewIndex(t);
|
int viewRow = rowObjectFilterModel.getViewIndex(t);
|
||||||
if (viewRow >= 0) {
|
if (viewRow >= 0) {
|
||||||
table.setRowSelectionInterval(viewRow, viewRow);
|
table.setRowSelectionInterval(viewRow, viewRow);
|
||||||
scrollToSelectedRow();
|
scrollToSelectedRow();
|
||||||
|
@ -785,7 +641,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
if (row < 0) {
|
if (row < 0) {
|
||||||
return null;
|
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);
|
List<ROW_OBJECT> list = new ArrayList<>(rows.length);
|
||||||
for (int row : rows) {
|
for (int row : rows) {
|
||||||
list.add(textFilterModel.getRowObject(row));
|
list.add(rowObjectFilterModel.getRowObject(row));
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
@ -814,7 +670,7 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
* @return true if in the view
|
* @return true if in the view
|
||||||
*/
|
*/
|
||||||
public boolean isInView(ROW_OBJECT o) {
|
public boolean isInView(ROW_OBJECT o) {
|
||||||
int rowIndex = textFilterModel.getRowIndex(o);
|
int rowIndex = rowObjectFilterModel.getRowIndex(o);
|
||||||
return rowIndex >= 0;
|
return rowIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -823,11 +679,48 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRowCount() {
|
public int getRowCount() {
|
||||||
return textFilterModel.getRowCount();
|
return rowObjectFilterModel.getRowCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getUnfilteredRowCount() {
|
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) {
|
private TableModelEvent translateEventForFilter(TableModelEvent event) {
|
||||||
int rowCount = textFilterModel.getUnfilteredRowCount();
|
int rowCount = rowObjectFilterModel.getUnfilteredRowCount();
|
||||||
if (rowCount == 0) {
|
if (rowCount == 0) {
|
||||||
return event; // nothing to translate--no data
|
return event; // nothing to translate--no data
|
||||||
}
|
}
|
||||||
|
@ -915,14 +808,14 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
|
|
||||||
if (firstRow == 0 && lastRow == rowCount - 1) {
|
if (firstRow == 0 && lastRow == rowCount - 1) {
|
||||||
firstRow = 0;
|
firstRow = 0;
|
||||||
lastRow = Math.max(0, textFilterModel.getRowCount() - 1);
|
lastRow = Math.max(0, rowObjectFilterModel.getRowCount() - 1);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// translate to the filtered view (from the wrapped model's full universe)
|
// translate to the filtered view (from the wrapped model's full universe)
|
||||||
firstRow = getViewRow(firstRow);
|
firstRow = getViewRow(firstRow);
|
||||||
lastRow = getViewRow(lastRow);
|
lastRow = getViewRow(lastRow);
|
||||||
}
|
}
|
||||||
return new TableModelEvent(textFilterModel, firstRow, lastRow, event.getColumn(),
|
return new TableModelEvent(rowObjectFilterModel, firstRow, lastRow, event.getColumn(),
|
||||||
event.getType());
|
event.getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -937,8 +830,8 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
isUpdatingModel = true;
|
isUpdatingModel = true;
|
||||||
if (textFilterModel instanceof WrappingTableModel) {
|
if (rowObjectFilterModel instanceof WrappingTableModel) {
|
||||||
WrappingTableModel tableModelWrapper = (WrappingTableModel) textFilterModel;
|
WrappingTableModel tableModelWrapper = (WrappingTableModel) rowObjectFilterModel;
|
||||||
tableModelWrapper.wrappedModelChangedFromTableChangedEvent();
|
tableModelWrapper.wrappedModelChangedFromTableChangedEvent();
|
||||||
}
|
}
|
||||||
filterField.alert();
|
filterField.alert();
|
||||||
|
@ -947,75 +840,16 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GTableFilterListener implements FilterListener {
|
private class GTableFilterListener implements FilterListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filterChanged(String text) {
|
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
|
// Static Methods
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
||||||
private static String generateFilterPreferenceKey(JTable jTable, String extension) {
|
private static String generateFilterPreferenceKey(JTable jTable, String extension) {
|
||||||
|
|
||||||
if (jTable instanceof GTable) {
|
if (jTable instanceof GTable) {
|
||||||
|
@ -1039,78 +873,4 @@ public class GTableFilterPanel<ROW_OBJECT> extends JPanel {
|
||||||
String clientName = filteredTrace[0].getClassName();
|
String clientName = filteredTrace[0].getClassName();
|
||||||
return clientName;
|
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 org.jdom.Element;
|
||||||
|
|
||||||
import docking.DockingWindowManager;
|
import docking.DockingWindowManager;
|
||||||
import docking.widgets.table.GTableFilterPanel;
|
|
||||||
import docking.widgets.table.RowObjectTableModel;
|
import docking.widgets.table.RowObjectTableModel;
|
||||||
import ghidra.framework.options.PreferenceState;
|
import ghidra.framework.options.PreferenceState;
|
||||||
import ghidra.framework.options.SaveState;
|
import ghidra.framework.options.SaveState;
|
||||||
|
@ -35,7 +34,7 @@ import ghidra.util.Msg;
|
||||||
* @param <R> the row type of the table.
|
* @param <R> the row type of the table.
|
||||||
*/
|
*/
|
||||||
public class ColumnFilterSaveManager<R> {
|
public class ColumnFilterSaveManager<R> {
|
||||||
private static final String COLUMN_FILTER_EXTENSION = ".ColumnFilterExtension";
|
|
||||||
private static final String COLUMN_FILTER_STATE = "COLUMN_FILTER_STATE";
|
private static final String COLUMN_FILTER_STATE = "COLUMN_FILTER_STATE";
|
||||||
|
|
||||||
private List<ColumnBasedTableFilter<R>> filters = new ArrayList<>();
|
private List<ColumnBasedTableFilter<R>> filters = new ArrayList<>();
|
||||||
|
@ -46,14 +45,15 @@ public class ColumnFilterSaveManager<R> {
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* 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 table The JTable that is filterable.
|
||||||
* @param model the TableModel that supports filtering.
|
* @param model the TableModel that supports filtering.
|
||||||
* @param dataSource the table's DataSource object.
|
* @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) {
|
RowObjectTableModel<R> model, Object dataSource) {
|
||||||
preferenceKey = panel.getPreferenceKey() + COLUMN_FILTER_EXTENSION;
|
preferenceKey = tablePreferenceKey + ColumnFilterManager.FILTER_EXTENSION;
|
||||||
loadFromPreferences(table, model, dataSource);
|
loadFromPreferences(table, model, dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ import docking.action.*;
|
||||||
import docking.widgets.OptionDialog;
|
import docking.widgets.OptionDialog;
|
||||||
import docking.widgets.dialogs.InputDialog;
|
import docking.widgets.dialogs.InputDialog;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
import docking.widgets.table.GTableFilterPanel;
|
|
||||||
import docking.widgets.table.RowObjectFilterModel;
|
import docking.widgets.table.RowObjectFilterModel;
|
||||||
import docking.widgets.table.columnfilter.*;
|
import docking.widgets.table.columnfilter.*;
|
||||||
import docking.widgets.table.constrainteditor.ColumnConstraintEditor;
|
import docking.widgets.table.constrainteditor.ColumnConstraintEditor;
|
||||||
|
@ -50,38 +49,38 @@ import utility.function.Callback;
|
||||||
public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
implements TableFilterDialogModelListener {
|
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 JPanel filterPanelContainer;
|
||||||
private List<ColumnFilterPanel> filterPanels = new ArrayList<>();
|
private List<ColumnFilterPanel> filterPanels = new ArrayList<>();
|
||||||
|
|
||||||
private Callback closeCallback;
|
private Callback closeCallback;
|
||||||
private GTableFilterPanel<R> gTableFilterPanel;
|
|
||||||
|
|
||||||
private JPanel bottomPanel;
|
private JPanel bottomPanel;
|
||||||
|
|
||||||
private JTable table;
|
|
||||||
private RowObjectFilterModel<R> tableModel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param gTableFilterPanel the GTableFilterPanel that launched this dialog.
|
* @param filterManager the filter manager
|
||||||
* @param table the table being filtered.
|
* @param table the table being filtered.
|
||||||
* @param tableModel the table model.
|
* @param tableModel the table model.
|
||||||
*/
|
*/
|
||||||
public ColumnFilterDialog(GTableFilterPanel<R> gTableFilterPanel, JTable table,
|
public ColumnFilterDialog(ColumnFilterManager<R> filterManager, JTable table,
|
||||||
RowObjectFilterModel<R> tableModel) {
|
RowObjectFilterModel<R> tableModel) {
|
||||||
super("Table Column Filters", WindowUtilities.areModalDialogsVisible(), true, true, false);
|
super("Table Column Filters", WindowUtilities.areModalDialogsVisible(), true, true, false);
|
||||||
this.gTableFilterPanel = gTableFilterPanel;
|
this.filterManager = filterManager;
|
||||||
this.table = table;
|
this.table = table;
|
||||||
this.tableModel = tableModel;
|
this.tableModel = tableModel;
|
||||||
|
|
||||||
ColumnBasedTableFilter<R> columnTableFilter = gTableFilterPanel.getColumnTableFilter();
|
ColumnBasedTableFilter<R> columnTableFilter = filterManager.getCurrentFilter();
|
||||||
|
|
||||||
filterModel =
|
dialogModel =
|
||||||
new ColumnFilterDialogModel<>(tableModel, table.getColumnModel(), columnTableFilter);
|
new ColumnFilterDialogModel<>(tableModel, table.getColumnModel(), columnTableFilter);
|
||||||
filterModel.addListener(this);
|
dialogModel.addListener(this);
|
||||||
|
|
||||||
setHelpLocation(new HelpLocation("Trees", "Column_Filters"));
|
setHelpLocation(new HelpLocation("Trees", "Column_Filters"));
|
||||||
addWorkPanel(buildMainPanel());
|
addWorkPanel(buildMainPanel());
|
||||||
|
@ -97,8 +96,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <R> boolean hasFilterableColumns(JTable table,
|
public static <R> boolean hasFilterableColumns(JTable table, RowObjectFilterModel<R> model) {
|
||||||
RowObjectFilterModel<R> model) {
|
|
||||||
return !ColumnFilterDialogModel.getAllColumnFilterData(model, table.getColumnModel())
|
return !ColumnFilterDialogModel.getAllColumnFilterData(model, table.getColumnModel())
|
||||||
.isEmpty();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
|
@ -119,7 +117,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
DockingAction saveAction = new DockingAction("Save", "Filter") {
|
DockingAction saveAction = new DockingAction("Save", "Filter") {
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabledForContext(ActionContext context) {
|
public boolean isEnabledForContext(ActionContext context) {
|
||||||
return !filterModel.getFilterRows().isEmpty() && filterModel.isValid();
|
return !dialogModel.getFilterRows().isEmpty() && dialogModel.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -140,15 +138,15 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
};
|
};
|
||||||
loadAction.setDescription("Load Filter");
|
loadAction.setDescription("Load Filter");
|
||||||
loadAction.setHelpLocation(new HelpLocation("Trees", "Load_Filter"));
|
loadAction.setHelpLocation(new HelpLocation("Trees", "Load_Filter"));
|
||||||
loadAction.setToolBarData(
|
loadAction.setToolBarData(new ToolBarData(Icons.OPEN_FOLDER_ICON));
|
||||||
new ToolBarData(Icons.OPEN_FOLDER_ICON));
|
|
||||||
addAction(loadAction);
|
addAction(loadAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveFilter() {
|
private void saveFilter() {
|
||||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(
|
String preferenceKey = filterManager.getPreferenceKey();
|
||||||
gTableFilterPanel, table, tableModel, filterModel.getDataSource());
|
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
|
||||||
ColumnBasedTableFilter<R> filter = filterModel.getTableColumnFilter();
|
table, tableModel, dialogModel.getDataSource());
|
||||||
|
ColumnBasedTableFilter<R> filter = dialogModel.getTableColumnFilter();
|
||||||
|
|
||||||
String defaultName = new Date().toString();
|
String defaultName = new Date().toString();
|
||||||
InputDialog dialog = new InputDialog("Save Filter", "Filter Name: ", defaultName, d -> {
|
InputDialog dialog = new InputDialog("Save Filter", "Filter Name: ", defaultName, d -> {
|
||||||
|
@ -174,13 +172,14 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
filter.setName(filterName);
|
filter.setName(filterName);
|
||||||
filterSaveManager.addFilter(filter);
|
filterSaveManager.addFilter(filter);
|
||||||
filterSaveManager.save();
|
filterSaveManager.save();
|
||||||
gTableFilterPanel.updateSavedFilters(filter, true);
|
filterManager.updateSavedFilters(filter, true);
|
||||||
filterModel.setFilter(filter);
|
dialogModel.setFilter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFilter() {
|
private void loadFilter() {
|
||||||
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(
|
String preferenceKey = filterManager.getPreferenceKey();
|
||||||
gTableFilterPanel, table, tableModel, filterModel.getDataSource());
|
ColumnFilterSaveManager<R> filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
|
||||||
|
table, tableModel, dialogModel.getDataSource());
|
||||||
List<ColumnBasedTableFilter<R>> savedFilters = filterSaveManager.getSavedFilters();
|
List<ColumnBasedTableFilter<R>> savedFilters = filterSaveManager.getSavedFilters();
|
||||||
if (savedFilters.isEmpty()) {
|
if (savedFilters.isEmpty()) {
|
||||||
Msg.showInfo(this, getComponent(), "No Saved Filters",
|
Msg.showInfo(this, getComponent(), "No Saved Filters",
|
||||||
|
@ -195,7 +194,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
|
|
||||||
ColumnBasedTableFilter<R> selectedFilter = archiveDialog.getSelectedColumnFilter();
|
ColumnBasedTableFilter<R> selectedFilter = archiveDialog.getSelectedColumnFilter();
|
||||||
if (selectedFilter != null) {
|
if (selectedFilter != null) {
|
||||||
filterModel.setFilter(selectedFilter);
|
dialogModel.setFilter(selectedFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,14 +222,12 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
bottomPanel = new JPanel(new BorderLayout());
|
bottomPanel = new JPanel(new BorderLayout());
|
||||||
JPanel innerPanel = new JPanel(new VerticalLayout(3));
|
JPanel innerPanel = new JPanel(new VerticalLayout(3));
|
||||||
|
|
||||||
JButton addAndConditionButton =
|
JButton addAndConditionButton = new JButton("Add AND condition", Icons.ADD_ICON);
|
||||||
new JButton("Add AND condition", Icons.ADD_ICON);
|
|
||||||
|
|
||||||
addAndConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.AND));
|
addAndConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.AND));
|
||||||
addAndConditionButton.setEnabled(true);
|
addAndConditionButton.setEnabled(true);
|
||||||
|
|
||||||
JButton addOrConditionButton =
|
JButton addOrConditionButton = new JButton("Add OR condition", Icons.ADD_ICON);
|
||||||
new JButton("Add OR condition", Icons.ADD_ICON);
|
|
||||||
|
|
||||||
addOrConditionButton.setHorizontalAlignment(SwingConstants.LEFT);
|
addOrConditionButton.setHorizontalAlignment(SwingConstants.LEFT);
|
||||||
addOrConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.OR));
|
addOrConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.OR));
|
||||||
|
@ -251,7 +248,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
}
|
}
|
||||||
sb.append("Column Filter");
|
sb.append("Column Filter");
|
||||||
|
|
||||||
ColumnBasedTableFilter<R> filter = filterModel.getTableColumnFilter();
|
ColumnBasedTableFilter<R> filter = dialogModel.getTableColumnFilter();
|
||||||
if (filter != null && filter.getName() != null) {
|
if (filter != null && filter.getName() != null) {
|
||||||
sb.append(": ").append(filter.getName());
|
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 and valid - prompt to apply filter.
|
||||||
// * Dialog state is different from applied filter, but invalid - prompt if should really close
|
// * Dialog state is different from applied filter, but invalid - prompt if should really close
|
||||||
|
|
||||||
if (!filterModel.hasUnappliedChanges()) {
|
if (!dialogModel.hasUnappliedChanges()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (dialogHasValidFilter()) {
|
if (dialogHasValidFilter()) {
|
||||||
|
@ -316,12 +313,12 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean dialogHasValidFilter() {
|
private boolean dialogHasValidFilter() {
|
||||||
return filterModel.getTableColumnFilter() != null;
|
return dialogModel.getTableColumnFilter() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void dialogClosed() {
|
protected void dialogClosed() {
|
||||||
filterModel.dispose();
|
dialogModel.dispose();
|
||||||
if (closeCallback != null) {
|
if (closeCallback != null) {
|
||||||
closeCallback.call();
|
closeCallback.call();
|
||||||
}
|
}
|
||||||
|
@ -339,28 +336,29 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearFilter() {
|
private void clearFilter() {
|
||||||
this.gTableFilterPanel.setColumnTableFilter(null);
|
filterManager.setFilter(null);
|
||||||
filterModel.clear();
|
dialogModel.clear();
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilter() {
|
private void applyFilter() {
|
||||||
ColumnBasedTableFilter<R> tableColumnFilter = filterModel.getTableColumnFilter();
|
ColumnBasedTableFilter<R> tableColumnFilter = dialogModel.getTableColumnFilter();
|
||||||
filterModel.setCurrentlyAppliedFilter(tableColumnFilter);
|
dialogModel.setCurrentlyAppliedFilter(tableColumnFilter);
|
||||||
this.gTableFilterPanel.setColumnTableFilter(tableColumnFilter);
|
filterManager.setFilter(tableColumnFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFilterRows() {
|
private void loadFilterRows() {
|
||||||
filterPanelContainer.removeAll();
|
filterPanelContainer.removeAll();
|
||||||
filterPanels.clear();
|
filterPanels.clear();
|
||||||
|
|
||||||
List<DialogFilterRow> filterRows = filterModel.getFilterRows();
|
List<DialogFilterRow> filterRows = dialogModel.getFilterRows();
|
||||||
for (int i = 0; i < filterRows.size(); i++) {
|
for (int i = 0; i < filterRows.size(); i++) {
|
||||||
DialogFilterRow filterRow = filterRows.get(i);
|
DialogFilterRow filterRow = filterRows.get(i);
|
||||||
ColumnFilterPanel panel = new ColumnFilterPanel(filterRow);
|
ColumnFilterPanel panel = new ColumnFilterPanel(filterRow);
|
||||||
if (i != 0) {
|
if (i != 0) {
|
||||||
filterPanelContainer.add(
|
LogicOperation op = filterRow.getLogicOperation();
|
||||||
createLogicalOperationLabel(filterRow.getLogicOperation()));
|
GLabel label = createLogicalOperationLabel(op);
|
||||||
|
filterPanelContainer.add(label);
|
||||||
}
|
}
|
||||||
filterPanelContainer.add(panel);
|
filterPanelContainer.add(panel);
|
||||||
filterPanels.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", SwingConstants.CENTER));
|
||||||
headerPanel.add(new GLabel("Filter Value", SwingConstants.CENTER));
|
headerPanel.add(new GLabel("Filter Value", SwingConstants.CENTER));
|
||||||
|
|
||||||
headerPanel.setBorder(new CompoundBorder(
|
headerPanel.setBorder(
|
||||||
BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
|
new CompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
|
||||||
BorderFactory.createEmptyBorder(4, 0, 4, 0)));
|
BorderFactory.createEmptyBorder(4, 0, 4, 0)));
|
||||||
return headerPanel;
|
return headerPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFilterCondition(LogicOperation logicalOperation) {
|
private void addFilterCondition(LogicOperation logicalOperation) {
|
||||||
filterModel.createFilterRow(logicalOperation);
|
dialogModel.createFilterRow(logicalOperation);
|
||||||
scrollFilterPanelToBottom();
|
scrollFilterPanelToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +408,7 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
void updateStatus() {
|
void updateStatus() {
|
||||||
setStatusText(getStatusMessage());
|
setStatusText(getStatusMessage());
|
||||||
|
|
||||||
boolean isValid = filterModel.isValid();
|
boolean isValid = dialogModel.isValid();
|
||||||
setOkEnabled(isValid);
|
setOkEnabled(isValid);
|
||||||
setApplyEnabled(isValid);
|
setApplyEnabled(isValid);
|
||||||
|
|
||||||
|
@ -423,11 +421,11 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
public void filterChanged(ColumnBasedTableFilter<R> newFilter) {
|
public void filterChanged(ColumnBasedTableFilter<R> newFilter) {
|
||||||
if (Objects.equals(newFilter, filterModel.getTableColumnFilter())) {
|
if (Objects.equals(newFilter, dialogModel.getTableColumnFilter())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getComponent().requestFocus(); // work around for java parenting bug where dialog appears behind
|
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",
|
int result = OptionDialog.showYesNoDialog(getComponent(), "Filter Changed",
|
||||||
"The filter has been changed externally.\n" +
|
"The filter has been changed externally.\n" +
|
||||||
" Do you want to update this editor and lose your current changes?");
|
" Do you want to update this editor and lose your current changes?");
|
||||||
|
@ -435,14 +433,14 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filterModel.setFilter(newFilter);
|
dialogModel.setFilter(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getStatusMessage() {
|
private String getStatusMessage() {
|
||||||
if (filterModel.isEmpty()) {
|
if (dialogModel.isEmpty()) {
|
||||||
return "Please add a filter condition!";
|
return "Please add a filter condition!";
|
||||||
}
|
}
|
||||||
if (!filterModel.isValid()) {
|
if (!dialogModel.isValid()) {
|
||||||
return "One or more filter values are invalid!";
|
return "One or more filter values are invalid!";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
@ -473,8 +471,6 @@ public class ColumnFilterDialog<R> extends ReusableDialogComponentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
void filterRemoved(ColumnBasedTableFilter<R> filter) {
|
void filterRemoved(ColumnBasedTableFilter<R> filter) {
|
||||||
gTableFilterPanel.updateSavedFilters(filter, false);
|
filterManager.updateSavedFilters(filter, false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,6 +193,12 @@ public class ConcurrentQBuilder<I, R> {
|
||||||
return this;
|
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) {
|
public ConcurrentQ<I, R> build(QCallback<I, R> callback) {
|
||||||
|
|
||||||
ConcurrentQ<I, R> concurrentQ = new ConcurrentQ<>(callback, getQueue(), getThreadPool(),
|
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
|
* 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 {
|
public class Lock {
|
||||||
private Thread owner;
|
private Thread owner;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue