getMatches(Address sourceAddress, Address destinationAddress);
/**
- * Removes a match from this match set. Note that this operation is only supported for built-in
- * match sets "Manual Matches" and "Implied Matches".
+ * Deletes the given match from this match set.
+ *
+ * Note: deleting an ACCEPTED 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.
+ *
+ * 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. Any applied markup item data
+ * will not be changed.
+ *
+ * @param match the match
+ */
+ public void deleteMatch(VTMatch match);
+
+ /**
+ * Removes a match from this match set.
+ *
+ * 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.
+ *
+ * Note: This method is deprecated. It unfortunately shares a very similar name with its
+ * replacement, {@link #deleteMatch(VTMatch)}. The replacement method will delete the match
+ * and the related association and markup items in the database, if the match is the last match
+ * to use that association. This deprecated method does not remove the remaining association or
+ * markup items. Historically, this method has been called after clearing the given match and
+ * its markup. Once this method has been deleted, clients will be responsible for managing the
+ * markup item state before calling {@link #deleteMatch(VTMatch)}.
+ *
* @param match the match to remove.
* @return true if the match was removed.
+ * @throws IllegalArgumentException if a non-database match is passed to this method
+ * @see #deleteMatch(VTMatch)
+ * @deprecated use {@link #deleteMatch(VTMatch)}
*/
+ @Deprecated(since = "11.2", forRemoval = true)
public boolean removeMatch(VTMatch match);
/**
- * Returns true if this match set supports removing matches.
- * @return true if this match set supports removing matches.
+ * Returns true
+ * @return true
+ * @deprecated this method now always returns true
*/
- public boolean hasRemovableMatches();
+ @Deprecated(since = "11.2", forRemoval = true)
+ public default boolean hasRemovableMatches() {
+ return true;
+ }
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java
index 77483d07d7..62f7dd0173 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java
@@ -41,7 +41,6 @@ public class RemoveMatchAction extends DockingAction {
super("Remove", VTPlugin.OWNER);
this.controller = controller;
-// setToolBarData(new ToolBarData(ICON, MENU_GROUP));
setPopupMenuData(new MenuData(new String[] { "Remove Match" }, ICON, MENU_GROUP));
setEnabled(false);
setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
@@ -64,17 +63,7 @@ public class RemoveMatchAction extends DockingAction {
}
VTMatchContext matchContext = (VTMatchContext) context;
List matches = matchContext.getSelectedMatches();
- if (matches.size() == 0) {
- return false;
- }
- if (!isRemovableMatch(matches.get(0))) {
- return false; // It must be a single manual match.
- }
- return true;
- }
-
- private boolean isRemovableMatch(VTMatch vtMatch) {
- return vtMatch.getMatchSet().hasRemovableMatches();
+ return !matches.isEmpty();
}
@Override
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java
index 99b2d93da5..d32197ce80 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java
@@ -282,12 +282,6 @@ public abstract class AbstractAddressRangeFilter extends AncillaryFilter
fireStatusChanged(getFilterStatus());
}
- @Override
- public void clearFilter() {
- lowerAddressRangeTextField.setText(MIN_ADDRESS_VALUE.toString());
- upperAddressRangeTextField.setText(MAX_ADDRESS_VALUE.toString());
- }
-
@Override
public JComponent getComponent() {
return component;
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java
index f14f0c08a5..afcf350500 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java
@@ -15,8 +15,7 @@
*/
package ghidra.feature.vt.gui.filters;
-import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.APPLIED;
-import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.NONE;
+import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.*;
import java.awt.Container;
import java.awt.LayoutManager;
@@ -151,13 +150,6 @@ public abstract class CheckBoxBasedAncillaryFilter extends AncillaryFilter
return FilterShortcutState.REQUIRES_CHECK;
}
- @Override
- public void clearFilter() {
- for (CheckBoxInfo info : checkBoxInfos) {
- info.setSelected(true);
- }
- }
-
@Override
public FilterState getFilterState() {
FilterState state = new FilterState(this);
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java
index 32c3b177b3..6b37eee795 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java
@@ -46,8 +46,6 @@ public abstract class Filter {
public abstract FilterEditingStatus getFilterStatus();
- public abstract void clearFilter();
-
public abstract JComponent getComponent();
public void dispose() {
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java
index 3c61b602bb..2eb3325286 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java
@@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,12 +17,13 @@ package ghidra.feature.vt.gui.filters;
public interface FilterDialogModel {
- public void addFilter( Filter filter );
-
- public void forceRefilter();
-
- /**
- * Will be called when the visibility of the dialog using this model has changed
- */
- public void dialogVisibilityChanged( boolean isVisible );
+ public void addFilter(Filter filter);
+
+ public void forceRefilter();
+
+ /**
+ * Will be called when the visibility of the dialog using this model has changed
+ * @param isVisible true if visible
+ */
+ public void dialogVisibilityChanged(boolean isVisible);
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java
index ed185935ae..8544644aba 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java
@@ -243,12 +243,6 @@ public class TagFilter extends AncillaryFilter {
excludedTags = getTagsFromText(tagText);
}
- @Override
- public void clearFilter() {
- excludedTags.clear();
- excludedTagsLabel.setText(ALL_TAGS_INCLUDED);
- }
-
@Override
public JComponent getComponent() {
return component;
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java
index 584b27f9b9..eff11789d7 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java
@@ -150,13 +150,14 @@ public class VTPlugin extends Plugin {
private void addCustomPlugins() {
- List names = new ArrayList<>(List.of("ghidra.features.codecompare.plugin"));
+ List names =
+ new ArrayList<>(List.of("ghidra.features.codecompare.plugin.FunctionComparisonPlugin"));
List plugins = tool.getManagedPlugins();
Set existingNames =
plugins.stream().map(c -> c.getName()).collect(Collectors.toSet());
// Note: we check to see if the plugins we want to add have already been added to the tool.
- // We should not needed to do this, but once the tool has been saved with the plugins added,
+ // We should not need to do this, but once the tool has been saved with the plugins added,
// they will get added again the next time the tool is loaded. Adding this check here seems
// easier than modifying the default to file to load the plugins, since the amount of xml
// required for that is non-trivial.
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java
index df31d03c17..158933b416 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java
@@ -127,12 +127,6 @@ public abstract class AbstractDoubleRangeFilter extends Filter
return component;
}
- @Override
- public void clearFilter() {
- lowerBoundField.setText(minValue.toString());
- upperBoundField.setText(maxValue.toString());
- }
-
@Override
public FilterEditingStatus getFilterStatus() {
FilterEditingStatus lowerStatus = lowerBoundField.getFilterStatus();
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java
index cca9d877b8..6114e3c967 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java
@@ -89,11 +89,6 @@ public class LengthFilter extends Filter {
return component;
}
- @Override
- public void clearFilter() {
- textField.setText(DEFAULT_FILTER_VALUE.toString());
- }
-
@Override
public FilterEditingStatus getFilterStatus() {
return textField.getFilterStatus();
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java
index fff753a973..43b4986444 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java
@@ -34,6 +34,8 @@ import javax.swing.table.*;
import docking.*;
import docking.action.builder.ActionBuilder;
import docking.widgets.table.*;
+import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
+import docking.widgets.table.columnfilter.ColumnFilterManager;
import docking.widgets.table.threaded.ThreadedTableModel;
import generic.theme.GIcon;
import ghidra.app.services.FunctionComparisonService;
@@ -80,6 +82,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
private AncillaryFilterDialogComponentProvider ancillaryFilterDialog;
private JButton ancillaryFilterButton;
+ private ColumnFilterManager columnFilterManager;
+ private VTColumnFilter vtColumnFilter;
private FilterIconFlashTimer iconTimer;
private Set> filters = new HashSet<>();
@@ -386,8 +390,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
JPanel innerPanel = new JPanel(new HorizontalLayout(4));
innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
- JComponent nameFilterPanel = createTextFilterPanel();
- parentPanel.add(nameFilterPanel, BorderLayout.CENTER);
+ JComponent textFilterPanel = createTextFilterPanel();
+ parentPanel.add(textFilterPanel, BorderLayout.CENTER);
parentPanel.add(innerPanel, BorderLayout.EAST);
JComponent scoreFilterPanel = createScoreFilterPanel();
@@ -409,13 +413,33 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
helpService.registerHelp(parentPanel, filterHelpLocation);
helpService.registerHelp(ancillaryFilterButton, filterHelpLocation);
+ JButton columnFilterButton = createColumnFilterButton();
+ innerPanel.add(columnFilterButton);
+
innerPanel.add(ancillaryFilterButton);
return parentPanel;
}
+ private JButton createColumnFilterButton() {
+
+ String preferenceKey =
+ matchesTable.getPreferenceKey() + ColumnFilterManager.FILTER_EXTENSION;
+ columnFilterManager = new ColumnFilterManager(matchesTable, matchesTableModel,
+ preferenceKey, this::updateColumnFilter);
+
+ vtColumnFilter = new VTColumnFilter(columnFilterManager.getCurrentFilter());
+ addFilter(vtColumnFilter);
+
+ return columnFilterManager.getConfigureButton();
+ }
+
+ private void updateColumnFilter() {
+ vtColumnFilter.setFilter(columnFilterManager.getCurrentFilter());
+ refilter();
+ }
+
private JComponent createTextFilterPanel() {
-// MatchNameFilter nameFilterPanel = new MatchNameFilter(controller, matchesTable);
AllTextFilter allTextFilter =
new AllTextFilter<>(controller, matchesTable, matchesTableModel);
allTextFilter.setName(TEXT_FILTER_NAME);
@@ -506,6 +530,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
}
ancillaryFilterDialog.dispose();
+
+ columnFilterManager.dispose();
}
@Override
@@ -1069,4 +1095,73 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
return list;
}
}
+
+ private class VTColumnFilter extends Filter {
+
+ private ColumnBasedTableFilter columnFilter;
+
+ VTColumnFilter(ColumnBasedTableFilter columnFilter) {
+ this.columnFilter = columnFilter;
+ }
+
+ void setFilter(ColumnBasedTableFilter 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 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 otherFilter) {
+ if (columnFilter == null) {
+ return false;
+ }
+ if (otherFilter instanceof VTColumnFilter otherColumnFilter) {
+ return columnFilter.isSubFilterOf(otherColumnFilter.columnFilter);
+ }
+ return false;
+ }
+
+ }
+
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java
index 3540fdddb0..1438d1dfc4 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java
@@ -15,11 +15,13 @@
*/
package ghidra.feature.vt.gui.task;
-import java.util.List;
+import java.util.*;
+import docking.widgets.OptionDialog;
import ghidra.feature.vt.api.db.VTMatchSetDB;
import ghidra.feature.vt.api.main.VTMatch;
import ghidra.feature.vt.api.main.VTSession;
+import ghidra.util.HelpLocation;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -38,26 +40,75 @@ public class RemoveMatchTask extends VtTask {
return true;
}
- private boolean removeMatches(TaskMonitor monitor) throws CancelledException {
+ private void removeMatches(TaskMonitor monitor) throws CancelledException {
+
monitor.setMessage("Removing matches");
- monitor.initialize(matches.size());
- boolean failed = false;
- for (VTMatch match : matches) {
+ int n = matches.size();
+ monitor.initialize(n);
+
+ //
+ // First remove all matches that will not require user prompting (those that are not
+ // accepted or they are not the last match for a shared association).
+ //
+ List list = new ArrayList<>(matches); // create a mutable list
+ Iterator it = list.iterator();
+ while (it.hasNext()) {
monitor.checkCancelled();
+ VTMatch match = it.next();
VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
- boolean matchRemoved = matchSet.removeMatch(match);
- if (!matchRemoved) {
- failed = true;
+ if (matchSet.removeMatch(match)) {
+ it.remove();
}
monitor.incrementProgress(1);
}
- monitor.setProgress(matches.size());
- if (failed) {
- reportError("One or more of your matches could not be removed." +
- "\nNote: You can't remove a match if it is currently accepted.");
+ if (list.isEmpty()) {
+ return;
+ }
+
+ //
+ // Now we have to ask the user if they wish to remove applied matches.
+ //
+ int delta = n - list.size();
+
+ //@formatter:off
+ String message = """
+ Deleted %d of %d matches.
+
+ The remaining %d matches are ACCEPTED. Do you wish to delete these matches and
+ leave any applied destination program markup in place?
+ (Press F1 to see more help details)
+ """.formatted(delta, n, list.size());
+ //@formatter:on
+
+ RemoveMatchDialog dialog = new RemoveMatchDialog(message);
+ if (!dialog.promptToDelete()) {
+ return;
+ }
+
+ it = list.iterator();
+ while (it.hasNext()) {
+ monitor.checkCancelled();
+ VTMatch match = it.next();
+ VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet();
+ matchSet.deleteMatch(match);
+ it.remove();
+ monitor.incrementProgress(1);
}
- return true;
}
+ private class RemoveMatchDialog extends OptionDialog {
+
+ RemoveMatchDialog(String message) {
+ super("Delete ACCEPTED Matches?", message, "Delete Accepted Matches", "Finish",
+ OptionDialog.QUESTION_MESSAGE, null, false);
+
+ setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match"));
+ }
+
+ boolean promptToDelete() {
+ int choice = super.show();
+ return choice == OptionDialog.OPTION_ONE; // "Delete Accepted Matches"
+ }
+ }
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java
index 78fbaf8ca2..443ec455e6 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java
@@ -125,11 +125,6 @@ public abstract class AbstractTextFilter extends Filter {
return component;
}
- @Override
- public void clearFilter() {
- textField.setText(defaultValue);
- }
-
@Override
public FilterEditingStatus getFilterStatus() {
return textField.getFilterStatus();
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java
index 57d3f69dc3..592daa6a57 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java
@@ -115,7 +115,7 @@ public class ImpliedMatchUtils {
VTMatchSet impliedMatchSet = session.getImpliedMatchSet();
for (VTMatch vtMatch : matches) {
if (vtMatch.getMatchSet() == impliedMatchSet) {
- impliedMatchSet.removeMatch(vtMatch);
+ impliedMatchSet.deleteMatch(vtMatch);
}
}
}
@@ -123,8 +123,7 @@ public class ImpliedMatchUtils {
/**
* Method for finding version tracking implied matches given an accepted matched
* function. Each referenced data and function that exist in equivalent sections
- * of the matched source and destination functions will added to the current
- * version tracking session as an implied match.
+ * of the matched source and destination functions will be returned in the given set.
*
* @param sourceFunction The matched function from the source program
* @param destinationFunction The matched function from the destination program
diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java
index 40d6046b74..bc17c2ea38 100644
--- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java
+++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java
@@ -18,7 +18,7 @@ package ghidra.feature.vt.api;
import static ghidra.feature.vt.db.VTTestUtils.*;
import static org.junit.Assert.*;
-import java.util.*;
+import java.util.Arrays;
import org.junit.*;
@@ -27,9 +27,8 @@ import ghidra.feature.vt.api.main.VTAssociationStatus;
import ghidra.feature.vt.api.main.VTMatch;
import ghidra.feature.vt.gui.plugin.*;
import ghidra.feature.vt.gui.task.AcceptMatchTask;
+import ghidra.feature.vt.gui.task.VtTask;
import ghidra.feature.vt.gui.util.VTOptionDefines;
-import ghidra.framework.model.DomainObjectChangedEvent;
-import ghidra.framework.model.DomainObjectListener;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramDB;
@@ -39,10 +38,7 @@ import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.test.*;
-import ghidra.util.exception.CancelledException;
import ghidra.util.exception.InvalidInputException;
-import ghidra.util.task.Task;
-import ghidra.util.task.TaskMonitor;
public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
@@ -53,13 +49,8 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
private ProgramDB sourceProgram;
private ProgramDB destinationProgram;
private VTPlugin plugin;
- private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
private Options options;
- public VTMatchAcceptTest() {
- super();
- }
-
@Before
public void setUp() throws Exception {
@@ -70,7 +61,6 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder();
destinationProgram = destinationBuilder.getProgram();
- destinationProgram.addListener(eventRecorder);
tool = env.getTool();
@@ -84,29 +74,21 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
runSwing(() -> controller.openVersionTrackingSession(session));
options = controller.getOptions();
- options.setBoolean(VTOptionDefines.AUTO_CREATE_IMPLIED_MATCH, false);
- options.setBoolean(VTOptionDefines.APPLY_FUNCTION_NAME_ON_ACCEPT, false);
- options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, false);
}
@After
public void tearDown() throws Exception {
- waitForBusyTool(tool);
- destinationProgram.flushEvents();
- waitForSwing();
-
env.dispose();
-
}
@Test
public void testAcceptWithApplyDataLabels() throws Exception {
//
- // BTW this test exposes a bug because the hook that runs when you apply data on accept was.
- // in a side effect, causing the destination address to be set. When the hook was changed to
- // not set the destination address, the accept task was not setting the destination address
- // as it should.
+ // This test exposes a bug because the hook that runs when you apply data on accept was
+ // exhibiting a side effect, causing the destination address to be set. When the hook was
+ // changed to not set the destination address, the accept task was not setting the
+ // destination address as it should.
//
options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true);
@@ -147,11 +129,9 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
}
}
- private void runTask(Task task) throws CancelledException {
-
- task.run(TaskMonitor.DUMMY);
- destinationProgram.flushEvents();
- waitForSwing();
+ private void runTask(VtTask task) {
+ controller.runVTTask(task);
+ waitForProgram(destinationProgram);
}
private Data setData(DataType dataType, int dtLength, Address address, Program program)
@@ -170,14 +150,4 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
}
return data;
}
-
- private class DomainObjectListenerRecorder implements DomainObjectListener {
-
- List events = new ArrayList();
-
- @Override
- public void domainObjectChanged(DomainObjectChangedEvent ev) {
- events.add(ev);
- }
- }
}
diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java
index 324d8f6ef4..aadd9288af 100644
--- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java
+++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java
@@ -69,7 +69,6 @@ public class VTMatchApplyTest extends AbstractGhidraHeadedIntegrationTest {
private ProgramDB destinationProgram;
private VTPlugin plugin;
- // TODO: debug
private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder();
@Before
diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java
new file mode 100644
index 0000000000..8d7e1c3078
--- /dev/null
+++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java
@@ -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 matches = matchSet.getMatches(srcAddr, destAddr);
+ assertTrue(matches.isEmpty());
+ }
+
+ private void assertMatchNotRemoved(VTMatchSet matchSet, Address srcAddr, Address destAddr) {
+ Collection 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);
+ }
+ }
+}
diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java
index 195c1975f1..dc3d3ed2e4 100644
--- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java
+++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java
@@ -27,6 +27,7 @@ import ghidra.util.task.TaskMonitor;
public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
+ private String name = "DummyTestProgramCorrelator";
private int matchCount = 1;
public DummyTestProgramCorrelator() {
@@ -84,8 +85,12 @@ public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator {
}
}
+ public void setName(String name) {
+ this.name = name;
+ }
+
@Override
public String getName() {
- return "DummyTestProgramCorrelator";
+ return name;
}
}
diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java
index 8bc8516901..fe2af6475c 100644
--- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java
+++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java
@@ -119,6 +119,7 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
assertEquals(VTEvent.MATCH_ADDED, events.get(0).getEventType());
}
+ @SuppressWarnings("removal") // ignore the warning until removeMatch() is removed
@Test
public void testEventsForRemovingLastMatchForAssociation() {
VTMatchSet manualMatchSet = db.getManualMatchSet();
@@ -134,6 +135,22 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
}
+ @Test
+ public void testEventsForDeletingLastMatchForAssociation() {
+ VTMatchSet manualMatchSet = db.getManualMatchSet();
+ clearEvents();
+ VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null);
+ VTMatch match = manualMatchSet.addMatch(matchInfo);
+ clearEvents();
+
+ manualMatchSet.deleteMatch(match);
+
+ assertEventCount(2);
+ assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType());
+ assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
+ }
+
+ @SuppressWarnings("removal") // ignore the warning until removeMatch() is removed
@Test
public void testEventsForRemovingNonLastMatchForAssociation() {
VTMatchSet manualMatchSet = db.getManualMatchSet();
@@ -149,6 +166,21 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase {
assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
}
+ @Test
+ public void testEventsForDeletingNonLastMatchForAssociation() {
+ VTMatchSet manualMatchSet = db.getManualMatchSet();
+ clearEvents();
+ VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null);
+ VTMatch match = manualMatchSet.addMatch(matchInfo);
+ clearEvents();
+
+ manualMatchSet.deleteMatch(match);
+
+ assertEventCount(2);
+ assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType());
+ assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType());
+ }
+
@Test
public void testEventsForRejectingMatch() throws VTAssociationStatusException {
VTMatchSet matchSet = createMatchSet();
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java
index bec4d6aedd..e2f5e3bbed 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java
@@ -24,29 +24,21 @@ import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.*;
import javax.swing.table.TableColumnModel;
-import javax.swing.table.TableModel;
import org.jdom.Element;
import docking.DockingWindowManager;
-import docking.menu.*;
import docking.widgets.EmptyBorderButton;
-import docking.widgets.EventTrigger;
import docking.widgets.filter.*;
import docking.widgets.label.GDLabel;
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
-import docking.widgets.table.columnfilter.ColumnFilterSaveManager;
-import docking.widgets.table.constraint.dialog.ColumnFilterDialog;
-import generic.theme.GIcon;
+import docking.widgets.table.columnfilter.ColumnFilterManager;
import ghidra.framework.options.PreferenceState;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
-import ghidra.util.datastruct.WeakDataStructureFactory;
-import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException;
import ghidra.util.task.SwingUpdateManager;
import help.HelpService;
-import resources.Icons;
import utilities.util.reflection.ReflectionUtilities;
import utility.function.Callback;
@@ -112,37 +104,26 @@ public class GTableFilterPanel extends JPanel {
public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield";
private static final String FILTER_STATE = "FILTER_STATE";
private static final String FILTER_EXTENSION = ".FilterExtension";
- private static final Icon FILTER_ON_ICON = new GIcon("icon.widget.filterpanel.filter.on");
- private static final Icon FILTER_OFF_ICON = new GIcon("icon.widget.filterpanel.filter.off");
- private static final Icon APPLY_FILTER_ICON = Icons.OPEN_FOLDER_ICON;
- private static final Icon CLEAR_FILTER_ICON = Icons.DELETE_ICON;
private JTable table;
- private RowObjectFilterModel textFilterModel;
+ private RowObjectFilterModel rowObjectFilterModel;
private JLabel searchLabel;
private FilterTextField filterField;
private FilterListener filterListener = new GTableFilterListener();
- private WeakSet listeners =
- WeakDataStructureFactory.createSingleThreadAccessWeakSet();
-
private FilterOptions filterOptions = new FilterOptions();
private TableTextFilterFactory filterFactory =
new DefaultTableTextFilterFactory<>(filterOptions);
private RowFilterTransformer transformer;
private TableFilter secondaryTableFilter;
- private ColumnBasedTableFilter columnTableFilter;
- private List> savedFilters = new ArrayList<>();
private EmptyBorderButton filterStateButton;
+ private ColumnFilterManager columnFilterManager;
+
private String uniquePreferenceKey;
- private MultiStateDockingAction> columnFilterAction;
- private ColumnFilterDialog columnFilterDialog;
- private ColumnBasedTableFilter lastUsedColumnFilter;
-
- private SwingUpdateManager updateManager = new SwingUpdateManager(250, 1000, () -> {
+ private SwingUpdateManager filterUpdater = new SwingUpdateManager(250, 1000, () -> {
String text = filterField.getText();
TableFilter tableFilter = filterFactory.getTableFilter(text, transformer);
@@ -151,8 +132,9 @@ public class GTableFilterPanel extends JPanel {
// result of a filter, the table does not know this and may update the wrong row data.
table.editingCanceled(null);
- textFilterModel.setTableFilter(
- getCombinedTableFilter(secondaryTableFilter, tableFilter, columnTableFilter));
+ ColumnBasedTableFilter columnFilter = columnFilterManager.getCurrentFilter();
+ rowObjectFilterModel.setTableFilter(
+ getCombinedTableFilter(secondaryTableFilter, tableFilter, columnFilter));
});
/** I'm a field so that my weak reference won't go away */
@@ -174,12 +156,12 @@ public class GTableFilterPanel extends JPanel {
@Override
public void columnRemoved(TableColumnModelEvent e) {
- updateTableContents();
+ filterUpdater.updateLater();
}
@Override
public void columnAdded(TableColumnModelEvent e) {
- updateTableContents();
+ filterUpdater.updateLater();
}
};
@@ -231,13 +213,16 @@ public class GTableFilterPanel extends JPanel {
String filterLabel) {
this.table = table;
- buildPanel(filterLabel);
-
uniquePreferenceKey = createUniqueFilterPreferenceKey(table);
transformer = new DefaultRowFilterTransformer<>(tableModel, table.getColumnModel());
- textFilterModel = installTableModel(tableModel);
+ rowObjectFilterModel = installTableModel(tableModel);
+
+ columnFilterManager = new ColumnFilterManager(table, rowObjectFilterModel,
+ getPreferenceKey(), filterUpdater::updateLater);
+
+ buildPanel(filterLabel);
TableColumnModel columnModel = table.getColumnModel();
columnModel.addColumnModelListener(columnModelListener);
@@ -246,12 +231,7 @@ public class GTableFilterPanel extends JPanel {
table.addPropertyChangeListener(badProgrammingPropertyChangeListener);
DockingWindowManager.registerComponentLoadedListener(this,
- (windowManager, provider) -> initialize(windowManager));
- }
-
- private void initialize(DockingWindowManager windowManager) {
- loadFilterPreference(windowManager);
- initializeSavedFilters();
+ (windowManager, provider) -> loadFilterPreference(windowManager));
}
private void loadFilterPreference(DockingWindowManager dockingWindowManager) {
@@ -283,7 +263,7 @@ public class GTableFilterPanel extends JPanel {
if (xmlElement != null) {
this.filterOptions = FilterOptions.restoreFromXML(xmlElement);
updateFilterFactory();
- updateTableContents();
+ filterUpdater.updateLater();
}
}
@@ -326,18 +306,7 @@ public class GTableFilterPanel extends JPanel {
* @param newFilter the ColumnTableFilter to use for filtering this table.
*/
public void setColumnTableFilter(ColumnBasedTableFilter newFilter) {
- if (Objects.equals(newFilter, this.columnTableFilter)) {
- return;
- }
- if (columnTableFilter != null && !columnTableFilter.isSaved()) {
- lastUsedColumnFilter = columnTableFilter;
- }
- columnTableFilter = newFilter;
- updateTableContents();
- updateColumnFilterButton();
- if (columnFilterDialog != null) {
- columnFilterDialog.filterChanged(newFilter);
- }
+ columnFilterManager.setFilter(newFilter);
}
/**
@@ -351,7 +320,7 @@ public class GTableFilterPanel extends JPanel {
*/
public void setFilterRowTransformer(RowFilterTransformer transformer) {
this.transformer = transformer;
- updateTableContents();
+ filterUpdater.updateLater();
}
/**
@@ -362,7 +331,7 @@ public class GTableFilterPanel extends JPanel {
*/
public void setSecondaryFilter(TableFilter tableFilter) {
this.secondaryTableFilter = tableFilter;
- updateTableContents();
+ filterUpdater.updateLater();
}
/**
@@ -373,7 +342,7 @@ public class GTableFilterPanel extends JPanel {
public void setFilterOptions(FilterOptions filterOptions) {
this.filterOptions = filterOptions;
updateFilterFactory();
- updateTableContents();
+ filterUpdater.updateLater();
doSaveState();
}
@@ -394,7 +363,7 @@ public class GTableFilterPanel extends JPanel {
add(buildFilterStateButton());
if (isTableColumnFilterableModel()) {
add(Box.createHorizontalStrut(5));
- add(buildColumnFilterStateButton());
+ add(columnFilterManager.getConfigureButton());
}
HelpService helpService = DockingWindowManager.getHelpService();
@@ -425,107 +394,6 @@ public class GTableFilterPanel extends JPanel {
return table.getModel() instanceof RowObjectFilterModel;
}
- @SuppressWarnings("unchecked")
- private JComponent buildColumnFilterStateButton() {
-
- RowObjectFilterModel tableModel =
- (RowObjectFilterModel) table.getModel();
- columnFilterAction =
- new NonToolbarMultiStateAction<>("Column Filter", "GTableFilterPanel") {
-
- @Override
- public void actionStateChanged(
- ActionState> 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 dynamicModel =
- (GDynamicColumnTableModel) model;
-
- ColumnFilterSaveManager saveManager =
- new ColumnFilterSaveManager<>(this, table, dynamicModel, dynamicModel.getDataSource());
- savedFilters = saveManager.getSavedFilters();
- Collections.reverse(savedFilters);
- updateColumnFilterButton();
- }
-
- private void updateColumnFilterButton() {
- List>> list = getActionStates();
-
- columnFilterAction.setActionStates(list);
- }
-
- private List>> getActionStates() {
- List>> 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 filter : savedFilters) {
- list.add(new ApplyFilterActionState(filter));
- }
- return list;
- }
-
- private void showFilterDialog(RowObjectFilterModel tableModel) {
- if (columnFilterDialog == null) {
- if (ColumnFilterDialog.hasFilterableColumns(table, tableModel)) {
- DockingWindowManager dockingWindowManager = DockingWindowManager.getInstance(table);
- loadFilterPreference(dockingWindowManager);
- columnFilterDialog = new ColumnFilterDialog<>(this, table, tableModel);
- }
- else {
- Msg.showError(this, this, "Column Filter Error",
- "This table contains no filterable columns!");
- return;
- }
-
- }
-
- columnFilterDialog.setCloseCallback(() -> {
- doSaveState();
- updateFilterFactory();
- columnFilterDialog = null;
- });
-
- DockingWindowManager.showDialog(GTableFilterPanel.this, columnFilterDialog);
- }
-
private void updateFilterFactory() {
filterStateButton.setIcon(filterOptions.getFilterStateIcon());
filterStateButton.setToolTipText(filterOptions.getFilterDescription());
@@ -587,17 +455,7 @@ public class GTableFilterPanel extends JPanel {
}
public RowObjectFilterModel getTableFilterModel() {
- return textFilterModel;
- }
-
- /** Convenience method to refilter the table's contents */
- private void updateTableContents() {
- updateManager.updateLater();
- notifyFilterChanged();
- }
-
- private void notifyFilterChanged() {
- listeners.forEach(callback -> callback.call());
+ return rowObjectFilterModel;
}
public void dispose() {
@@ -609,13 +467,11 @@ public class GTableFilterPanel extends JPanel {
columnModel.removeColumnModelListener(columnModelListener);
columnModelListener = null;
- if (columnFilterDialog != null) {
- columnFilterDialog.dispose();
- }
+ columnFilterManager.dispose();
table.removePropertyChangeListener(badProgrammingPropertyChangeListener);
- updateManager.dispose();
+ filterUpdater.dispose();
if (table instanceof GTable) {
((GTable) table).dispose();
}
@@ -696,7 +552,7 @@ public class GTableFilterPanel extends JPanel {
return viewRow;
}
- return textFilterModel.getModelRow(viewRow);
+ return rowObjectFilterModel.getModelRow(viewRow);
}
/**
@@ -710,7 +566,7 @@ public class GTableFilterPanel extends JPanel {
* @return the row in the table for the given model row.
*/
public int getViewRow(int modelRow) {
- return textFilterModel.getViewRow(modelRow);
+ return rowObjectFilterModel.getViewRow(modelRow);
}
/**
@@ -720,7 +576,7 @@ public class GTableFilterPanel extends JPanel {
* @return the row object matching the given index
*/
public ROW_OBJECT getRowObject(int viewRow) {
- ROW_OBJECT rowObject = textFilterModel.getRowObject(viewRow);
+ ROW_OBJECT rowObject = rowObjectFilterModel.getRowObject(viewRow);
return rowObject;
}
@@ -736,7 +592,7 @@ public class GTableFilterPanel extends JPanel {
return;
}
- int viewRow = textFilterModel.getViewIndex(t);
+ int viewRow = rowObjectFilterModel.getViewIndex(t);
if (viewRow >= 0) {
table.setRowSelectionInterval(viewRow, viewRow);
scrollToSelectedRow();
@@ -785,7 +641,7 @@ public class GTableFilterPanel extends JPanel {
if (row < 0) {
return null;
}
- return textFilterModel.getRowObject(row);
+ return rowObjectFilterModel.getRowObject(row);
}
/**
@@ -801,7 +657,7 @@ public class GTableFilterPanel extends JPanel {
List list = new ArrayList<>(rows.length);
for (int row : rows) {
- list.add(textFilterModel.getRowObject(row));
+ list.add(rowObjectFilterModel.getRowObject(row));
}
return list;
}
@@ -814,7 +670,7 @@ public class GTableFilterPanel extends JPanel {
* @return true if in the view
*/
public boolean isInView(ROW_OBJECT o) {
- int rowIndex = textFilterModel.getRowIndex(o);
+ int rowIndex = rowObjectFilterModel.getRowIndex(o);
return rowIndex >= 0;
}
@@ -823,11 +679,48 @@ public class GTableFilterPanel extends JPanel {
}
public int getRowCount() {
- return textFilterModel.getRowCount();
+ return rowObjectFilterModel.getRowCount();
}
public int getUnfilteredRowCount() {
- return textFilterModel.getUnfilteredRowCount();
+ return rowObjectFilterModel.getUnfilteredRowCount();
+ }
+
+ /**
+ * Generates a key used to store user filter configuration state. You can override this
+ * method to generate unique keys yourself. You are required to override this method if
+ * you create multiple versions of a filter panel from the same place in your code, as
+ * multiple instances created in the same place will cause them all to share the same key and
+ * thus to have the same filter settings when they are created initially.
+ *
+ * As an example, consider a plugin that creates n
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 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 extends JPanel {
}
private TableModelEvent translateEventForFilter(TableModelEvent event) {
- int rowCount = textFilterModel.getUnfilteredRowCount();
+ int rowCount = rowObjectFilterModel.getUnfilteredRowCount();
if (rowCount == 0) {
return event; // nothing to translate--no data
}
@@ -915,14 +808,14 @@ public class GTableFilterPanel extends JPanel {
if (firstRow == 0 && lastRow == rowCount - 1) {
firstRow = 0;
- lastRow = Math.max(0, textFilterModel.getRowCount() - 1);
+ lastRow = Math.max(0, rowObjectFilterModel.getRowCount() - 1);
}
else {
// translate to the filtered view (from the wrapped model's full universe)
firstRow = getViewRow(firstRow);
lastRow = getViewRow(lastRow);
}
- return new TableModelEvent(textFilterModel, firstRow, lastRow, event.getColumn(),
+ return new TableModelEvent(rowObjectFilterModel, firstRow, lastRow, event.getColumn(),
event.getType());
}
}
@@ -937,8 +830,8 @@ public class GTableFilterPanel extends JPanel {
}
isUpdatingModel = true;
- if (textFilterModel instanceof WrappingTableModel) {
- WrappingTableModel tableModelWrapper = (WrappingTableModel) textFilterModel;
+ if (rowObjectFilterModel instanceof WrappingTableModel) {
+ WrappingTableModel tableModelWrapper = (WrappingTableModel) rowObjectFilterModel;
tableModelWrapper.wrappedModelChangedFromTableChangedEvent();
}
filterField.alert();
@@ -947,75 +840,16 @@ public class GTableFilterPanel extends JPanel {
}
private class GTableFilterListener implements FilterListener {
-
@Override
public void filterChanged(String text) {
- updateTableContents();
+ filterUpdater.updateLater();
}
}
- /**
- * Generates a key used to store user filter configuration state. You can override this
- * method to generate unique keys yourself. You are required to override this method if
- * you create multiple versions of a filter panel from the same place in your code, as
- * multiple instances created in the same place will cause them all to share the same key and
- * thus to have the same filter settings when they are created initially.
- *
- * As an example, consider a plugin that creates n
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 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 filter, boolean add) {
- if (add) {
- ArrayList> list = new ArrayList<>();
- list.add(filter);
- list.addAll(savedFilters);
- savedFilters = list;
- if (filter.isEquivalent(columnTableFilter)) {
- setColumnTableFilter(filter);
- }
- }
- else {
- savedFilters.remove(filter);
- }
-
- updateColumnFilterButton();
- }
-
//==================================================================================================
// Static Methods
//==================================================================================================
+
private static String generateFilterPreferenceKey(JTable jTable, String extension) {
if (jTable instanceof GTable) {
@@ -1039,78 +873,4 @@ public class GTableFilterPanel extends JPanel {
String clientName = filteredTrace[0].getClassName();
return clientName;
}
-
-//==================================================================================================
-// Inner Classes
-//==================================================================================================
-
- private abstract class ColumnFilterActionState
- extends ActionState> {
-
- ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter filter) {
- super(name, icon, filter);
- }
-
- abstract void performAction();
- }
-
- String getFilterName(ColumnBasedTableFilter 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 filter) {
- super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter);
- }
-
- @Override
- void performAction() {
- showFilterDialog(textFilterModel);
- }
- }
-
- private class ApplyFilterActionState extends ColumnFilterActionState {
- public ApplyFilterActionState(ColumnBasedTableFilter filter) {
- super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter);
- }
-
- @Override
- void performAction() {
- setColumnTableFilter(getUserData());
- }
- }
-
- private class ApplyLastUsedActionState extends ColumnFilterActionState {
- public ApplyLastUsedActionState(ColumnBasedTableFilter filter) {
- super("Apply Last Unsaved", FILTER_ON_ICON, filter);
- }
-
- @Override
- void performAction() {
- setColumnTableFilter(getUserData());
- }
- }
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java
new file mode 100644
index 0000000000..0eeb3a67d8
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java
@@ -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 the row type
+ */
+public class ColumnFilterManager {
+
+ 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> columnFilterAction;
+ private JButton configureButton;
+ private ColumnFilterDialog columnFilterDialog;
+
+ private ColumnBasedTableFilter lastUsedFilter;
+ private ColumnBasedTableFilter currentFilter;
+ private List> savedFilters = new ArrayList<>();
+
+ private JTable table;
+ private RowObjectFilterModel rowObjectFilterModel;
+ private String preferenceKey;
+ private Callback filterChangedCallback;
+
+ public ColumnFilterManager(JTable table, RowObjectFilterModel 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 dynamicModel =
+ (GDynamicColumnTableModel) model;
+
+ ColumnFilterSaveManager saveManager = new ColumnFilterSaveManager<>(
+ preferenceKey, table, dynamicModel, dynamicModel.getDataSource());
+
+ savedFilters = saveManager.getSavedFilters();
+ Collections.reverse(savedFilters);
+ updateColumnFilterButton();
+ }
+
+ public ColumnBasedTableFilter getCurrentFilter() {
+ return currentFilter;
+ }
+
+ public JButton getConfigureButton() {
+ return configureButton;
+ }
+
+ public String getPreferenceKey() {
+ return preferenceKey;
+ }
+
+ public void setFilter(ColumnBasedTableFilter 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 filter, boolean add) {
+
+ if (add) {
+ ArrayList> 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> 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>> list = getActionStates();
+ columnFilterAction.setActionStates(list);
+ }
+
+ private List>> getActionStates() {
+ List>> 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 filter : savedFilters) {
+ list.add(new ApplyFilterActionState(filter));
+ }
+ return list;
+ }
+
+ private void showFilterDialog(RowObjectFilterModel 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> {
+
+ ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter filter) {
+ super(name, icon, filter);
+ }
+
+ abstract void performAction();
+ }
+
+ String getFilterName(ColumnBasedTableFilter 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 filter) {
+ super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter);
+ }
+
+ @Override
+ void performAction() {
+ showFilterDialog(rowObjectFilterModel);
+ }
+ }
+
+ private class ApplyFilterActionState extends ColumnFilterActionState {
+ public ApplyFilterActionState(ColumnBasedTableFilter filter) {
+ super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter);
+ }
+
+ @Override
+ void performAction() {
+ setFilter(getUserData());
+ }
+ }
+
+ private class ApplyLastUsedActionState extends ColumnFilterActionState {
+ public ApplyLastUsedActionState(ColumnBasedTableFilter filter) {
+ super("Apply Last Unsaved", FILTER_ON_ICON, filter);
+ }
+
+ @Override
+ void performAction() {
+ setFilter(getUserData());
+ }
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java
index 12c09f1faf..bfa795d0e8 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java
@@ -23,7 +23,6 @@ import javax.swing.JTable;
import org.jdom.Element;
import docking.DockingWindowManager;
-import docking.widgets.table.GTableFilterPanel;
import docking.widgets.table.RowObjectTableModel;
import ghidra.framework.options.PreferenceState;
import ghidra.framework.options.SaveState;
@@ -35,7 +34,7 @@ import ghidra.util.Msg;
* @param the row type of the table.
*/
public class ColumnFilterSaveManager {
- private static final String COLUMN_FILTER_EXTENSION = ".ColumnFilterExtension";
+
private static final String COLUMN_FILTER_STATE = "COLUMN_FILTER_STATE";
private List> filters = new ArrayList<>();
@@ -46,14 +45,15 @@ public class ColumnFilterSaveManager {
/**
* Constructor
*
- * @param panel The GTableFilterPanel for the table.
+ * @param tablePreferenceKey the key used to save table settings. This is used to make a
+ * preference key for saving the column filters.
* @param table The JTable that is filterable.
* @param model the TableModel that supports filtering.
* @param dataSource the table's DataSource object.
*/
- public ColumnFilterSaveManager(GTableFilterPanel panel, JTable table,
+ public ColumnFilterSaveManager(String tablePreferenceKey, JTable table,
RowObjectTableModel model, Object dataSource) {
- preferenceKey = panel.getPreferenceKey() + COLUMN_FILTER_EXTENSION;
+ preferenceKey = tablePreferenceKey + ColumnFilterManager.FILTER_EXTENSION;
loadFromPreferences(table, model, dataSource);
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java
index ee036b3f00..9c94cd40d3 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java
@@ -29,7 +29,6 @@ import docking.action.*;
import docking.widgets.OptionDialog;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.label.GLabel;
-import docking.widgets.table.GTableFilterPanel;
import docking.widgets.table.RowObjectFilterModel;
import docking.widgets.table.columnfilter.*;
import docking.widgets.table.constrainteditor.ColumnConstraintEditor;
@@ -50,38 +49,38 @@ import utility.function.Callback;
public class ColumnFilterDialog extends ReusableDialogComponentProvider
implements TableFilterDialogModelListener {
- private final ColumnFilterDialogModel filterModel;
+ private ColumnFilterManager filterManager;
+ private ColumnFilterDialogModel dialogModel;
+
+ private JTable table;
+ private RowObjectFilterModel tableModel;
private JPanel filterPanelContainer;
private List filterPanels = new ArrayList<>();
private Callback closeCallback;
- private GTableFilterPanel gTableFilterPanel;
private JPanel bottomPanel;
- private JTable table;
- private RowObjectFilterModel tableModel;
-
/**
* Constructor
- *
- * @param gTableFilterPanel the GTableFilterPanel that launched this dialog.
+ *
+ * @param filterManager the filter manager
* @param table the table being filtered.
* @param tableModel the table model.
*/
- public ColumnFilterDialog(GTableFilterPanel gTableFilterPanel, JTable table,
+ public ColumnFilterDialog(ColumnFilterManager filterManager, JTable table,
RowObjectFilterModel tableModel) {
super("Table Column Filters", WindowUtilities.areModalDialogsVisible(), true, true, false);
- this.gTableFilterPanel = gTableFilterPanel;
+ this.filterManager = filterManager;
this.table = table;
this.tableModel = tableModel;
- ColumnBasedTableFilter columnTableFilter = gTableFilterPanel.getColumnTableFilter();
+ ColumnBasedTableFilter columnTableFilter = filterManager.getCurrentFilter();
- filterModel =
+ dialogModel =
new ColumnFilterDialogModel<>(tableModel, table.getColumnModel(), columnTableFilter);
- filterModel.addListener(this);
+ dialogModel.addListener(this);
setHelpLocation(new HelpLocation("Trees", "Column_Filters"));
addWorkPanel(buildMainPanel());
@@ -97,10 +96,9 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
updateStatus();
}
- public static boolean hasFilterableColumns(JTable table,
- RowObjectFilterModel model) {
+ public static boolean hasFilterableColumns(JTable table, RowObjectFilterModel model) {
return !ColumnFilterDialogModel.getAllColumnFilterData(model, table.getColumnModel())
- .isEmpty();
+ .isEmpty();
}
private void addClearFilterButton() {
@@ -119,7 +117,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
DockingAction saveAction = new DockingAction("Save", "Filter") {
@Override
public boolean isEnabledForContext(ActionContext context) {
- return !filterModel.getFilterRows().isEmpty() && filterModel.isValid();
+ return !dialogModel.getFilterRows().isEmpty() && dialogModel.isValid();
}
@Override
@@ -140,15 +138,15 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
};
loadAction.setDescription("Load Filter");
loadAction.setHelpLocation(new HelpLocation("Trees", "Load_Filter"));
- loadAction.setToolBarData(
- new ToolBarData(Icons.OPEN_FOLDER_ICON));
+ loadAction.setToolBarData(new ToolBarData(Icons.OPEN_FOLDER_ICON));
addAction(loadAction);
}
private void saveFilter() {
- ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(
- gTableFilterPanel, table, tableModel, filterModel.getDataSource());
- ColumnBasedTableFilter filter = filterModel.getTableColumnFilter();
+ String preferenceKey = filterManager.getPreferenceKey();
+ ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
+ table, tableModel, dialogModel.getDataSource());
+ ColumnBasedTableFilter filter = dialogModel.getTableColumnFilter();
String defaultName = new Date().toString();
InputDialog dialog = new InputDialog("Save Filter", "Filter Name: ", defaultName, d -> {
@@ -174,13 +172,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
filter.setName(filterName);
filterSaveManager.addFilter(filter);
filterSaveManager.save();
- gTableFilterPanel.updateSavedFilters(filter, true);
- filterModel.setFilter(filter);
+ filterManager.updateSavedFilters(filter, true);
+ dialogModel.setFilter(filter);
}
private void loadFilter() {
- ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(
- gTableFilterPanel, table, tableModel, filterModel.getDataSource());
+ String preferenceKey = filterManager.getPreferenceKey();
+ ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey,
+ table, tableModel, dialogModel.getDataSource());
List> savedFilters = filterSaveManager.getSavedFilters();
if (savedFilters.isEmpty()) {
Msg.showInfo(this, getComponent(), "No Saved Filters",
@@ -195,7 +194,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
ColumnBasedTableFilter selectedFilter = archiveDialog.getSelectedColumnFilter();
if (selectedFilter != null) {
- filterModel.setFilter(selectedFilter);
+ dialogModel.setFilter(selectedFilter);
}
}
@@ -223,14 +222,12 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
bottomPanel = new JPanel(new BorderLayout());
JPanel innerPanel = new JPanel(new VerticalLayout(3));
- JButton addAndConditionButton =
- new JButton("Add AND condition", Icons.ADD_ICON);
+ JButton addAndConditionButton = new JButton("Add AND condition", Icons.ADD_ICON);
addAndConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.AND));
addAndConditionButton.setEnabled(true);
- JButton addOrConditionButton =
- new JButton("Add OR condition", Icons.ADD_ICON);
+ JButton addOrConditionButton = new JButton("Add OR condition", Icons.ADD_ICON);
addOrConditionButton.setHorizontalAlignment(SwingConstants.LEFT);
addOrConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.OR));
@@ -251,7 +248,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
}
sb.append("Column Filter");
- ColumnBasedTableFilter filter = filterModel.getTableColumnFilter();
+ ColumnBasedTableFilter filter = dialogModel.getTableColumnFilter();
if (filter != null && filter.getName() != null) {
sb.append(": ").append(filter.getName());
}
@@ -275,7 +272,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
// * Dialog state is different from applied filter and valid - prompt to apply filter.
// * Dialog state is different from applied filter, but invalid - prompt if should really close
- if (!filterModel.hasUnappliedChanges()) {
+ if (!dialogModel.hasUnappliedChanges()) {
return true;
}
if (dialogHasValidFilter()) {
@@ -316,12 +313,12 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
}
private boolean dialogHasValidFilter() {
- return filterModel.getTableColumnFilter() != null;
+ return dialogModel.getTableColumnFilter() != null;
}
@Override
protected void dialogClosed() {
- filterModel.dispose();
+ dialogModel.dispose();
if (closeCallback != null) {
closeCallback.call();
}
@@ -339,28 +336,29 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
}
private void clearFilter() {
- this.gTableFilterPanel.setColumnTableFilter(null);
- filterModel.clear();
+ filterManager.setFilter(null);
+ dialogModel.clear();
updateStatus();
}
private void applyFilter() {
- ColumnBasedTableFilter tableColumnFilter = filterModel.getTableColumnFilter();
- filterModel.setCurrentlyAppliedFilter(tableColumnFilter);
- this.gTableFilterPanel.setColumnTableFilter(tableColumnFilter);
+ ColumnBasedTableFilter tableColumnFilter = dialogModel.getTableColumnFilter();
+ dialogModel.setCurrentlyAppliedFilter(tableColumnFilter);
+ filterManager.setFilter(tableColumnFilter);
}
private void loadFilterRows() {
filterPanelContainer.removeAll();
filterPanels.clear();
- List filterRows = filterModel.getFilterRows();
+ List filterRows = dialogModel.getFilterRows();
for (int i = 0; i < filterRows.size(); i++) {
DialogFilterRow filterRow = filterRows.get(i);
ColumnFilterPanel panel = new ColumnFilterPanel(filterRow);
if (i != 0) {
- filterPanelContainer.add(
- createLogicalOperationLabel(filterRow.getLogicOperation()));
+ LogicOperation op = filterRow.getLogicOperation();
+ GLabel label = createLogicalOperationLabel(op);
+ filterPanelContainer.add(label);
}
filterPanelContainer.add(panel);
filterPanels.add(panel);
@@ -382,14 +380,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
headerPanel.add(new GLabel("Filter", SwingConstants.CENTER));
headerPanel.add(new GLabel("Filter Value", SwingConstants.CENTER));
- headerPanel.setBorder(new CompoundBorder(
- BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
- BorderFactory.createEmptyBorder(4, 0, 4, 0)));
+ headerPanel.setBorder(
+ new CompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER),
+ BorderFactory.createEmptyBorder(4, 0, 4, 0)));
return headerPanel;
}
private void addFilterCondition(LogicOperation logicalOperation) {
- filterModel.createFilterRow(logicalOperation);
+ dialogModel.createFilterRow(logicalOperation);
scrollFilterPanelToBottom();
}
@@ -410,7 +408,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
void updateStatus() {
setStatusText(getStatusMessage());
- boolean isValid = filterModel.isValid();
+ boolean isValid = dialogModel.isValid();
setOkEnabled(isValid);
setApplyEnabled(isValid);
@@ -423,11 +421,11 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
}
public void filterChanged(ColumnBasedTableFilter newFilter) {
- if (Objects.equals(newFilter, filterModel.getTableColumnFilter())) {
+ if (Objects.equals(newFilter, dialogModel.getTableColumnFilter())) {
return;
}
getComponent().requestFocus(); // work around for java parenting bug where dialog appears behind
- if (filterModel.hasUnappliedChanges()) {
+ if (dialogModel.hasUnappliedChanges()) {
int result = OptionDialog.showYesNoDialog(getComponent(), "Filter Changed",
"The filter has been changed externally.\n" +
" Do you want to update this editor and lose your current changes?");
@@ -435,14 +433,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
return;
}
}
- filterModel.setFilter(newFilter);
+ dialogModel.setFilter(newFilter);
}
private String getStatusMessage() {
- if (filterModel.isEmpty()) {
+ if (dialogModel.isEmpty()) {
return "Please add a filter condition!";
}
- if (!filterModel.isValid()) {
+ if (!dialogModel.isValid()) {
return "One or more filter values are invalid!";
}
return "";
@@ -473,8 +471,6 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider
}
void filterRemoved(ColumnBasedTableFilter filter) {
- gTableFilterPanel.updateSavedFilters(filter, false);
-
+ filterManager.updateSavedFilters(filter, false);
}
-
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java
index 61a1e8cd19..e41233be05 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java
@@ -193,6 +193,12 @@ public class ConcurrentQBuilder {
return this;
}
+ /**
+ * Builds the final {@link ConcurrentQ}.
+ *
+ * @param callback the callback for processing each job
+ * @return the new queue
+ */
public ConcurrentQ build(QCallback callback) {
ConcurrentQ concurrentQ = new ConcurrentQ<>(callback, getQueue(), getThreadPool(),
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java
index 7a42b51b82..8b799f4e09 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java
@@ -17,7 +17,7 @@ package ghidra.util;
/**
* Ghidra synchronization lock. This class allows creation of named locks for
- * synchroniing modification of multiple tables in the Ghidra database.
+ * synchronizing modification of multiple tables in the Ghidra database.
*/
public class Lock {
private Thread owner;