GT-2763 - Table Sorting - fixed bug that triggered filtering to take

place for individual add/remove operations; fixed bug that caused loss
of added/removed items
This commit is contained in:
dragonmacher 2019-05-07 18:56:51 -04:00
parent da5f009c71
commit 445c7ca03e
32 changed files with 720 additions and 143 deletions

View File

@ -16,8 +16,7 @@
package ghidra.app.plugin.core.symtable;
import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import javax.swing.*;
import javax.swing.event.TableModelListener;
@ -213,5 +212,41 @@ class SymbolPanel extends JPanel {
}
return list;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getEnclosingInstance().hashCode();
result = prime * result + ((list == null) ? 0 : list.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
NameOnlyRowTransformer other = (NameOnlyRowTransformer) obj;
if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
return false;
}
if (!Objects.equals(list, other.list)) {
return false;
}
return true;
}
private SymbolPanel getEnclosingInstance() {
return SymbolPanel.this;
}
}
}

View File

@ -243,6 +243,43 @@ public class VTMarkupItemsTableModel extends AddressBasedTableModel<VTMarkupItem
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getEnclosingInstance().hashCode();
result = prime * result + ((appliedFilters == null) ? 0 : appliedFilters.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MarkupTablePassthroughFilter other = (MarkupTablePassthroughFilter) obj;
if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
return false;
}
if (!Objects.equals(appliedFilters, other.appliedFilters)) {
return false;
}
return true;
}
private VTMarkupItemsTableModel getEnclosingInstance() {
return VTMarkupItemsTableModel.this;
}
}
// column for selecting/editing?
@ -478,7 +515,7 @@ public class VTMarkupItemsTableModel extends AddressBasedTableModel<VTMarkupItem
private static final String NO_SOURCE_TEXT = "None";
private GColumnRenderer<String> sourceCellRenderer = new AbstractGColumnRenderer<String>() {
private GColumnRenderer<String> sourceCellRenderer = new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -543,30 +580,28 @@ public class VTMarkupItemsTableModel extends AddressBasedTableModel<VTMarkupItem
static class IsInDBTableColumn
extends AbstractProgramBasedDynamicTableColumn<VTMarkupItem, Boolean> {
private GColumnRenderer<Boolean> isInDBCellRenderer =
new AbstractGColumnRenderer<Boolean>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
private GColumnRenderer<Boolean> isInDBCellRenderer = new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
Object value = data.getValue();
Object value = data.getValue();
boolean isInDB = ((Boolean) value).booleanValue();
boolean isInDB = ((Boolean) value).booleanValue();
GTableCellRenderingData renderData =
data.copyWithNewValue(isInDB ? "yes" : null);
GTableCellRenderingData renderData = data.copyWithNewValue(isInDB ? "yes" : null);
JLabel renderer = (JLabel) super.getTableCellRendererComponent(renderData);
renderer.setOpaque(true);
JLabel renderer = (JLabel) super.getTableCellRendererComponent(renderData);
renderer.setOpaque(true);
return renderer;
}
return renderer;
}
@Override
public String getFilterString(Boolean t, Settings settings) {
boolean isInDB = t.booleanValue();
return isInDB ? "yes" : "";
}
};
@Override
public String getFilterString(Boolean t, Settings settings) {
boolean isInDB = t.booleanValue();
return isInDB ? "yes" : "";
}
};
@Override
public String getColumnName() {

View File

@ -203,6 +203,41 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getEnclosingInstance().hashCode();
result = prime * result + ((appliedFilters == null) ? 0 : appliedFilters.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MatchTablePassthroughFilter other = (MatchTablePassthroughFilter) obj;
if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
return false;
}
if (!Objects.equals(appliedFilters, other.appliedFilters)) {
return false;
}
return true;
}
private AbstractVTMatchTableModel getEnclosingInstance() {
return AbstractVTMatchTableModel.this;
}
}
static class MarkupStatusColumnComparator implements Comparator<VTMatch> {
@ -397,7 +432,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
return 55;
}
private GColumnRenderer<VTScore> renderer = new AbstractGColumnRenderer<VTScore>() {
private GColumnRenderer<VTScore> renderer = new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -457,7 +492,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
return 55;
}
private GColumnRenderer<VTScore> renderer = new AbstractGColumnRenderer<VTScore>() {
private GColumnRenderer<VTScore> renderer = new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -550,7 +585,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
}
private GColumnRenderer<DisplayableLabel> labelCellRenderer =
new AbstractGColumnRenderer<DisplayableLabel>() {
new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -681,7 +716,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
}
private GColumnRenderer<DisplayableAddress> addressCellRenderer =
new AbstractGColumnRenderer<DisplayableAddress>() {
new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -788,7 +823,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
}
private GColumnRenderer<DisplayableLabel> labelCellRenderer =
new AbstractGColumnRenderer<DisplayableLabel>() {
new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
@ -921,7 +956,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel<V
}
private GColumnRenderer<DisplayableAddress> addressCellRenderer =
new AbstractGColumnRenderer<DisplayableAddress>() {
new AbstractGColumnRenderer<>() {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {

View File

@ -15,12 +15,13 @@
*/
package docking.widgets.filter;
import java.util.Objects;
import java.util.regex.Pattern;
public abstract class AbstractPatternTextFilter implements TextFilter {
protected final String filterText;
private Pattern filterPattern;
protected Pattern filterPattern;
protected AbstractPatternTextFilter(String filterText) {
this.filterText = filterText;
@ -67,6 +68,45 @@ public abstract class AbstractPatternTextFilter implements TextFilter {
return filterPattern;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((filterPattern == null) ? 0 : filterPattern.hashCode());
result = prime * result + ((filterText == null) ? 0 : filterText.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AbstractPatternTextFilter other = (AbstractPatternTextFilter) obj;
String myPattern = getPatternString();
String otherPattern = other.getPatternString();
if (!myPattern.equals(otherPattern)) {
return false;
}
if (!Objects.equals(filterText, other.filterText)) {
return false;
}
return true;
}
private String getPatternString() {
return filterPattern == null ? "" : filterPattern.pattern();
}
@Override
public String toString() {
//@formatter:off

View File

@ -24,13 +24,8 @@ import ghidra.util.UserSearchUtils;
*/
public class ContainsTextFilter extends MatchesPatternTextFilter {
private boolean caseSensitive;
private boolean allowGlobbing;
public ContainsTextFilter(String filterText, boolean caseSensitive, boolean allowGlobbing) {
super(filterText);
this.caseSensitive = caseSensitive;
this.allowGlobbing = allowGlobbing;
super(filterText, caseSensitive, allowGlobbing);
}
@Override

View File

@ -15,6 +15,8 @@
*/
package docking.widgets.filter;
import java.util.Objects;
public class InvertedTextFilter implements TextFilter {
private final TextFilter filter;
@ -39,4 +41,30 @@ public class InvertedTextFilter implements TextFilter {
return filter.getFilterText();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((filter == null) ? 0 : filter.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InvertedTextFilter other = (InvertedTextFilter) obj;
if (!Objects.equals(filter, other.filter)) {
return false;
}
return true;
}
}

View File

@ -24,14 +24,9 @@ import ghidra.util.UserSearchUtils;
*/
public class MatchesExactlyTextFilter extends MatchesPatternTextFilter {
private boolean caseSensitive;
private boolean allowGlobbing;
public MatchesExactlyTextFilter(String filterText, boolean caseSensitive,
boolean allowGlobbing) {
super(filterText);
this.caseSensitive = caseSensitive;
this.allowGlobbing = allowGlobbing;
super(filterText, caseSensitive, allowGlobbing);
}
@Override

View File

@ -22,12 +22,55 @@ import java.util.regex.Pattern;
*/
public abstract class MatchesPatternTextFilter extends AbstractPatternTextFilter {
public MatchesPatternTextFilter(String filterText) {
protected boolean caseSensitive;
protected boolean allowGlobbing;
public MatchesPatternTextFilter(String filterText, boolean caseSensitive,
boolean allowGlobbing) {
super(filterText);
this.caseSensitive = caseSensitive;
this.allowGlobbing = allowGlobbing;
}
@Override
public boolean matches(String text, Pattern pattern) {
return pattern.matcher(text).matches();
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + (allowGlobbing ? 1231 : 1237);
result = prime * result + (caseSensitive ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
if (!super.equals(obj)) {
return false;
}
MatchesPatternTextFilter other = (MatchesPatternTextFilter) obj;
if (allowGlobbing != other.allowGlobbing) {
return false;
}
if (caseSensitive != other.caseSensitive) {
return false;
}
return true;
}
}

View File

@ -24,13 +24,8 @@ import ghidra.util.UserSearchUtils;
*/
public class StartsWithTextFilter extends MatchesPatternTextFilter {
private boolean caseSensitive;
private boolean allowGlobbing;
public StartsWithTextFilter(String filterText, boolean caseSensitive, boolean allowGlobbing) {
super(filterText);
this.caseSensitive = caseSensitive;
this.allowGlobbing = allowGlobbing;
super(filterText, caseSensitive, allowGlobbing);
}
@Override

View File

@ -232,9 +232,6 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
* fact that the data searched is retrieved from {@link #getModelData()}, which may be
* filtered.
*
* <p>Warning: if the this model has no sort applied, then -1 will be returned. You can call
* {@link #isSorted()} to know when this will happen.
*
* @param rowObject The object for which to search.
* @return the index of the item in the data returned by
*/

View File

@ -15,8 +15,7 @@
*/
package docking.widgets.table;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
/**
* Combines multiple Table Filters into a single TableFilter that can be applied. All contained
@ -101,4 +100,32 @@ public class CombinedTableFilter<T> implements TableFilter<T> {
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((filters == null) ? 0 : filters.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CombinedTableFilter<?> other = (CombinedTableFilter<?>) obj;
if (!Objects.equals(filters, other.filters)) {
return false;
}
return true;
}
}

View File

@ -15,8 +15,7 @@
*/
package docking.widgets.table;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import javax.swing.JLabel;
import javax.swing.table.TableColumnModel;
@ -27,7 +26,7 @@ import ghidra.util.table.column.GColumnRenderer.ColumnConstraintFilterMode;
public class DefaultRowFilterTransformer<ROW_OBJECT> implements RowFilterTransformer<ROW_OBJECT> {
private List<String> columnData = new ArrayList<String>();
private List<String> columnData = new ArrayList<>();
private TableColumnModel columnModel;
private final RowObjectTableModel<ROW_OBJECT> model;
@ -129,4 +128,41 @@ public class DefaultRowFilterTransformer<ROW_OBJECT> implements RowFilterTransfo
(GColumnRenderer<Object>) column.getColumnRenderer();
return columnRenderer;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((columnData == null) ? 0 : columnData.hashCode());
result = prime * result + ((columnModel == null) ? 0 : columnModel.hashCode());
result = prime * result + ((model == null) ? 0 : model.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DefaultRowFilterTransformer<?> other = (DefaultRowFilterTransformer<?>) obj;
if (!Objects.equals(columnData, other.columnData)) {
return false;
}
if (!Objects.equals(columnModel, other.columnModel)) {
return false;
}
if (!Objects.equals(model, other.model)) {
return false;
}
return true;
}
}

View File

@ -20,8 +20,8 @@ import java.util.List;
import docking.widgets.filter.*;
public class DefaultTableTextFilterFactory<ROW_OBJECT> implements
TableTextFilterFactory<ROW_OBJECT> {
public class DefaultTableTextFilterFactory<ROW_OBJECT>
implements TableTextFilterFactory<ROW_OBJECT> {
private final TextFilterFactory textFilterFactory;
private final boolean inverted;
@ -40,7 +40,7 @@ public class DefaultTableTextFilterFactory<ROW_OBJECT> implements
TableFilter<ROW_OBJECT> tableFilter = getBaseFilter(text, transformer);
if (inverted && tableFilter != null) {
tableFilter = new InvertedTableFilter<ROW_OBJECT>(tableFilter);
tableFilter = new InvertedTableFilter<>(tableFilter);
}
return tableFilter;
}
@ -55,14 +55,14 @@ public class DefaultTableTextFilterFactory<ROW_OBJECT> implements
if (textFilter == null) {
return null;
}
return new TableTextFilter<ROW_OBJECT>(textFilter, transformer);
return new TableTextFilter<>(textFilter, transformer);
}
private TableFilter<ROW_OBJECT> getMultiWordTableFilter(String text,
RowFilterTransformer<ROW_OBJECT> transformer) {
List<TextFilter> filters = new ArrayList<TextFilter>();
List<TextFilter> filters = new ArrayList<>();
TermSplitter splitter = filterOptions.getTermSplitter();
for (String term : splitter.split(text)) {
TextFilter textFilter = textFilterFactory.getTextFilter(term);
@ -70,7 +70,7 @@ public class DefaultTableTextFilterFactory<ROW_OBJECT> implements
filters.add(textFilter);
}
}
return new MultiTextFilterTableFilter<ROW_OBJECT>(text, filters, transformer,
return new MultiTextFilterTableFilter<>(filters, transformer,
filterOptions.getMultitermEvaluationMode());
}
}

View File

@ -15,6 +15,8 @@
*/
package docking.widgets.table;
import java.util.Objects;
public class InvertedTableFilter<ROW_OBJECT> implements TableFilter<ROW_OBJECT> {
private final TableFilter<ROW_OBJECT> filter;
@ -34,4 +36,31 @@ public class InvertedTableFilter<ROW_OBJECT> implements TableFilter<ROW_OBJECT>
return !filter.acceptsRow(rowObject);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((filter == null) ? 0 : filter.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InvertedTableFilter<?> other = (InvertedTableFilter<?>) obj;
if (!Objects.equals(filter, other.filter)) {
return false;
}
return true;
}
}

View File

@ -16,6 +16,7 @@
package docking.widgets.table;
import java.util.List;
import java.util.Objects;
import docking.widgets.filter.MultitermEvaluationMode;
import docking.widgets.filter.TextFilter;
@ -23,13 +24,11 @@ import docking.widgets.filter.TextFilter;
public class MultiTextFilterTableFilter<ROW_OBJECT> implements TableFilter<ROW_OBJECT> {
private final List<TextFilter> filters;
private final String text;
private final RowFilterTransformer<ROW_OBJECT> transformer;
private final MultitermEvaluationMode evalMode;
public MultiTextFilterTableFilter(String text, List<TextFilter> filters,
public MultiTextFilterTableFilter(List<TextFilter> filters,
RowFilterTransformer<ROW_OBJECT> transformer, MultitermEvaluationMode evalMode) {
this.text = text;
this.filters = filters;
this.transformer = transformer;
this.evalMode = evalMode;
@ -90,4 +89,41 @@ public class MultiTextFilterTableFilter<ROW_OBJECT> implements TableFilter<ROW_O
// @formatter:on
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((evalMode == null) ? 0 : evalMode.hashCode());
result = prime * result + ((filters == null) ? 0 : filters.hashCode());
result = prime * result + ((transformer == null) ? 0 : transformer.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MultiTextFilterTableFilter<?> other = (MultiTextFilterTableFilter<?>) obj;
if (evalMode != other.evalMode) {
return false;
}
if (!Objects.equals(filters, other.filters)) {
return false;
}
if (!Objects.equals(transformer, other.transformer)) {
return false;
}
return true;
}
}

View File

@ -64,9 +64,6 @@ public interface RowObjectFilterModel<ROW_OBJECT> extends RowObjectTableModel<RO
* <p>This operation will be O(n) unless the implementation is sorted, in which case the
* operation is O(log n), as it uses a binary search.
*
* <p>Note: if a sorted implementation is moved to an unsorted state, then -1 will be returned
* from this method.
*
* @param t the item
* @return the view index
*/
@ -80,9 +77,6 @@ public interface RowObjectFilterModel<ROW_OBJECT> extends RowObjectTableModel<RO
* <p>This operation will be O(n) unless the implementation is sorted, in which case the
* operation is O(log n), as it uses a binary search.
*
* <p>Note: if a sorted implementation is moved to an unsorted state, then -1 will be returned
* from this method.
*
* @param t the item
* @return the model index
*/

View File

@ -16,6 +16,7 @@
package docking.widgets.table;
import java.util.List;
import java.util.Objects;
import docking.widgets.filter.TextFilter;
@ -56,6 +57,38 @@ public class TableTextFilter<ROW_OBJECT> implements TableFilter<ROW_OBJECT> {
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((textFilter == null) ? 0 : textFilter.hashCode());
result = prime * result + ((transformer == null) ? 0 : transformer.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TableTextFilter<?> other = (TableTextFilter<?>) obj;
if (!Objects.equals(textFilter, other.textFilter)) {
return false;
}
if (!Objects.equals(transformer, other.transformer)) {
return false;
}
return true;
}
@Override
public String toString() {
return getClass().getSimpleName() + " - filter='" + textFilter.getFilterText() + "'";

View File

@ -318,10 +318,6 @@ public class ColumnBasedTableFilter<R> implements TableFilter<R> {
list.add(constraintSet);
}
public boolean isEmpty() {
return list.isEmpty();
}
boolean acceptsRow(R rowObject) {
for (ColumnConstraintSet<R, ?> constraintSet : list) {
if (!constraintSet.accepts(rowObject, tableFilterContext)) {

View File

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,17 +15,30 @@
*/
package docking.widgets.table.threaded;
import ghidra.util.task.TaskMonitor;
import java.util.List;
import docking.widgets.table.AddRemoveListItem;
import ghidra.util.task.TaskMonitor;
public class AddRemoveJob<T> extends TableUpdateJob<T> {
AddRemoveJob(ThreadedTableModel<T, ?> model, List<AddRemoveListItem<T>> addRemoveList,
TaskMonitor monitor) {
super(model, monitor);
setForceFilter(false); // the item will do its own sorting and filtering
this.addRemoveList = addRemoveList;
}
@Override
public synchronized boolean filter() {
//
// This is a request to fully filter the table's data (like when the filter changes).
// In this case, we had disabled 'force filter', as the sorting did not need it.
// However, when the client asks to filter, make sure we filter.
//
boolean jobIsStillRunning = super.filter();
if (jobIsStillRunning) {
setForceFilter(true); // reset, since we had turned it off above; now we have to filter
}
return jobIsStillRunning;
}
}

View File

@ -38,4 +38,23 @@ public class NullTableFilter<ROW_OBJECT> implements TableFilter<ROW_OBJECT> {
// to filter, which doesn't make sense if this is meant to only be used by itself.
return false;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return true;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}

View File

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,8 +15,8 @@
*/
package docking.widgets.table.threaded;
import ghidra.util.task.TaskMonitor;
import docking.widgets.table.TableSortingContext;
import ghidra.util.task.TaskMonitor;
public class SortJob<T> extends TableUpdateJob<T> {
@ -30,10 +29,15 @@ public class SortJob<T> extends TableUpdateJob<T> {
@Override
public synchronized boolean filter() {
boolean canFilter = super.filter();
if (canFilter) {
//
// This is a request to fully filter the table's data (like when the filter changes).
// In this case, we had disabled 'force filter', as the sorting did not need it.
// However, when the client asks to filter, make sure we filter.
//
boolean jobIsStillRunning = super.filter();
if (jobIsStillRunning) {
setForceFilter(true); // reset, since we had turned it off above; now we have to filter
}
return canFilter;
return jobIsStillRunning;
}
}

View File

@ -83,9 +83,13 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
}
TableData<ROW_OBJECT> copy() {
return copy(source);
}
TableData<ROW_OBJECT> copy(TableData<ROW_OBJECT> newSource) {
List<ROW_OBJECT> dataCopy = new ArrayList<>(data);
TableData<ROW_OBJECT> newData = new TableData<>(dataCopy, sortContext);
newData.source = source;
newData.source = newSource;
newData.tableFilter = tableFilter;
newData.ID = ID; // it is a copy, but represents the same data
return newData;
@ -128,7 +132,8 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
}
/**
* Uses the current sort to perform a fast lookup of the given item in the given list
* Uses the current sort to perform a fast lookup of the given item in the given list when
* sorted; a brute-force lookup when not sorted
* @param t the item
* @return the index
*/
@ -258,9 +263,11 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
* @return true if the source data nor the filter are different that what is used by this object.
*/
boolean matchesFilter(TableFilter<ROW_OBJECT> filter) {
// O.K., we are derived from the same source data, if the filter is the same, then there
// is no need to refilter
// is no need to refilter.
//
// Note: if a given filter does not override equals(), then this really means that they
// must be the same filter for this method to return true
return SystemUtilities.isEqual(tableFilter, filter);
}
@ -287,6 +294,16 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
return source.isUnrelatedTo(other);
}
/**
* Returns the ID of this table data. It is possible that two data instances of this class
* that have the same ID are considered to be the same data.
*
* @return the ID
*/
int getId() {
return ID;
}
/**
* Returns the root dataset for this data and all its ancestors.
* @return the root dataset for this data and all its ancestors.

View File

@ -166,7 +166,8 @@ public class TableUpdateJob<T> {
*
* @param item the add/remove item to add to the list of items to be processed in the add/remove
* phase of this job.
* @param the maximum number of add/remove jobs to queue before performing a full reload
* @param maxAddRemoveCount the maximum number of add/remove jobs to queue before performing
* a full reload
*/
public synchronized void addRemove(AddRemoveListItem<T> item, int maxAddRemoveCount) {
if (currentState != NOT_RUNNING) {
@ -219,17 +220,17 @@ public class TableUpdateJob<T> {
/**
* Tells the job that the filter criteria has changed. This method can be called on
* the currently running job as well as the pending job. If called on the running job, the effect
* depends on the running job's state:
* the currently running job as well as the pending job. If called on the running job, the
* effect depends on the running job's state:
* <ul>
* <li>If the filter state hasn't happened yet, then nothing needs to be done as this job
* will filter later anyway.
* will filter later anyway.
* <li>If the filter state has already been started or completed, then this method
* attempts to stop the current process phase and cause the state machine to return to the
* filter phase.
* attempts to stop the current process phase and cause the state machine to
* return to the filter phase.
* <li>If the current job has already entered the DONE state, then the filter cannot take
* effect in this job and a false value is returned to indicate the
* filter was not handled by this job.
* effect in this job and a false value is returned to indicate the filter was
* not handled by this job.
* </ul>
* @return true if the filter can be processed by this job, false if this job is essentially already
* completed and therefor cannot perform the filter job.
@ -239,7 +240,7 @@ public class TableUpdateJob<T> {
return false;
}
if (hasFiltered()) {
// the user has requested a new filter, and we've already filtered, so we need to filter again
// the user has requested a new filter; we've already filtered, so filter again
monitor.cancel();
pendingRequestedState = FILTERING;
}
@ -457,6 +458,11 @@ public class TableUpdateJob<T> {
/** True if the sort applied to the table is not the same as that in the source dataset */
private boolean tableSortDiffersFromSourceData() {
// Note: at this point in time we do not check to see if the table is user-unsorted. It
// doesn't seem to hurt to leave the original source data sorted, even if the
// current context is 'unsorted'. In that case, this method will return true,
// that the sorts are different. But, later in this job, we check the new sort and
// do not perform sorting when 'unsorted'
return !SystemUtilities.isEqual(sourceData.getSortContext(), model.getSortingContext());
}
@ -600,23 +606,24 @@ public class TableUpdateJob<T> {
List<T> list = filterSourceData.getData();
List<T> result = model.doFilter(list, lastSortContext, monitor);
if (result != list) { // yes, '=='
if (result == list) { // yes, '=='
// no filtering took place
updatedData = filterSourceData;
}
else {
// the derived data is sorted the same as the source data
TableSortingContext<T> sortContext = filterSourceData.getSortContext();
updatedData = TableData.createSubDataset(filterSourceData, result, sortContext);
updatedData.setTableFilter(model.getTableFilter());
}
else {
// no filtering took place
updatedData = filterSourceData;
}
monitor.setMessage(
"Done filtering " + model.getName() + " (" + updatedData.size() + " rows)");
}
private void copyCurrentFilterData() {
TableData<T> currentFilteredData = getCurrentFilteredData();
updatedData = currentFilteredData.copy(); // copy so we don't modify the UIs version
updatedData = currentFilteredData.copy(sourceData); // copy; don't modify the UI's version
// We are re-using the filtered data, so use too its sort
lastSortContext = updatedData.getSortContext();

View File

@ -486,7 +486,11 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
SystemUtilities.assertThisIsTheSwingThread("Must be called on the Swing thread");
boolean dataChanged = (this.filteredData.size() != filteredData.size());
//@formatter:off
// The data is changed when it is filtered OR when an item has been added or removed
boolean dataChanged = this.filteredData.getId() != filteredData.getId() ||
this.filteredData.size() != filteredData.size();
//@formatter:on
this.allData = allData;
this.filteredData = filteredData;

View File

@ -21,6 +21,7 @@ import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import org.junit.Before;
import org.junit.Test;
@ -32,12 +33,15 @@ import ghidra.docking.spy.SpyEventRecorder;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.task.TaskMonitor;
/**
* Specifically tests the sub-filtering behavior of the {@link ThreadedTableModel}, as well
* as some other more complicated filtering combinations
*/
public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
private SpyEventRecorder recorder = new SpyEventRecorder(getClass().getSimpleName());
private SpyTaskMonitor monitor = new SpyTaskMonitor();
private SpyTextFilter<Long> spyFilter;
private ThreadedTableModelListener spyLoadListener = new SpyTableModelListener();
@Override
protected TestDataKeyModel createTestModel() {
@ -82,9 +86,11 @@ public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
Boolean.FALSE.toString());
waitForTableModel(model);
}
// must run in Swing so that we do not mutate listeners while events are broadcasting
runSwing(() -> model.addThreadedTableModelListener(spyLoadListener));
@Override
protected TestThreadedTableModelListener createListener() {
return new TestThreadedTableModelListener(model, recorder);
}
@Override
@ -370,6 +376,89 @@ public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
assertRowCount(4); // matching values (for both filters): two, ten, ten, ten
}
@Test
public void testCombinedFilter_AddRemove_ItemPassesFilter_FilterJobStateDoesNotRun() {
//
// Tests that an item can be added/removed via addObject()/removeObject() *and* that,
// with a *combined* filter installed, the *filter* phase of the TableLoadJob will *NOT*
// get run. (The add/remove operation should perform filtering and sorting outside of
// the normal TableLoadJob's state machine.)
//
int fullCount = getRowCount();
createCombinedFilterWithEmptyTextFilter(new AllPassesTableFilter());
assertFilteredEntireModel();
assertRowCount(fullCount); // our filter passes everything
// call addObject()
long newId = fullCount + 1;
spyFilter.reset();
addItemToModel(newId);
assertNumberOfItemsPassedThroughFilter(1); // **this is the important check**
assertRowCount(fullCount + 1); // our filter passes everything
}
@Test
public void testCombinedFilter_AddRemove_ItemFailsFilter_FilterJobStateDoesNotRun() {
//
// Tests that an item can be added/removed via addObject()/removeObject() *and* that,
// with a *combined* filter installed, the *filter* phase of the TableLoadJob will *NOT*
// get run. (The add/remove operation should perform filtering and sorting outside of
// the normal TableLoadJob's state machine.)
//
int fullCount = getRowCount();
// use filter to limit any new items added from passing
Predicate<Long> predicate = l -> l < fullCount;
PredicateTableFilter noNewItemsPassFilter = new PredicateTableFilter(predicate);
createCombinedFilterWithEmptyTextFilter(noNewItemsPassFilter);
assertFilteredEntireModel();
assertRowCount(fullCount); // our filter passes everything
// call addObject()
long newId = fullCount + 1;
spyFilter.reset();
addItemToModel(newId);
assertNumberOfItemsPassedThroughFilter(1); // **this is the important check**
assertRowCount(fullCount); // the new item should not be added
}
@Test
public void testCombinedFilter_AddRemove_ItemPassesFilter_RefilterThenUndo() throws Exception {
//
// Bug Case: This was a case where a table (like the Symbol Table) that uses permanent
// combined filters would lose items inserted via the addObject() call. The
// issue is that the job was not properly updating the table's full source
// data, only its filtered data. Thus, when a job triggered a reload from
// the original source data, the value would be lost.
//
int fullCount = getRowCount();
createCombinedFilterWithEmptyTextFilter(new AllPassesTableFilter());
assertFilteredEntireModel();
assertRowCount(fullCount); // our filter passes everything
// call addObject()
long newId = fullCount + 1;
addItemToModel(newId);
assertRowCount(fullCount + 1); // our filter passes everything
filterOnRawColumnValue(newId);
assertRowCount(1);
createCombinedFilterWithEmptyTextFilter(new AllPassesTableFilter());
assertRowCount(fullCount + 1);
}
//==================================================================================================
// Private Methods
//==================================================================================================
@ -449,6 +538,27 @@ public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
waitForSwing();
}
private void createCombinedFilterWithEmptyTextFilter(TableFilter<Long> nonTextFilter) {
// the row objects are Long values that are 0-based one-up index values
DefaultRowFilterTransformer<Long> transformer =
new DefaultRowFilterTransformer<>(model, table.getColumnModel());
TextFilter allPassesFilter = new EmptyTextFilter();
spyFilter = new SpyTextFilter<>(allPassesFilter, transformer, recorder);
CombinedTableFilter<Long> combinedFilter =
new CombinedTableFilter<>(spyFilter, nonTextFilter, null);
recorder.record("Before setting the new filter");
runSwing(() -> model.setTableFilter(combinedFilter));
recorder.record("\tafter setting filter");
waitForNotBusy();
waitForTableModel(model);
waitForSwing();
}
private void createCombinedStartsWithFilter(String filterValue,
TableFilter<Long> secondFilter) {
@ -507,6 +617,10 @@ public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
assertTrue("The table did not filter data when it should have", spyFilter.hasFiltered());
}
@Override
protected void record(String message) {
recorder.record("Test - " + message);
}
//==================================================================================================
// Inner Classes
//==================================================================================================
@ -570,28 +684,53 @@ public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
}
private class SpyTableModelListener implements ThreadedTableModelListener {
private class EmptyTextFilter implements TextFilter {
@Override
public void loadPending() {
recorder.record("Swing - model load pending");
public boolean matches(String text) {
return true;
}
@Override
public void loadingStarted() {
recorder.record("Swing - model load started");
public String getFilterText() {
return null;
}
@Override
public void loadingFinished(boolean wasCancelled) {
if (wasCancelled) {
recorder.record("Swing - model load cancelled");
}
else {
recorder.record("Swing - model load finsished; size: " + model.getRowCount());
}
public boolean isSubFilterOf(TextFilter filter) {
return true;
}
}
private class AllPassesTableFilter implements TableFilter<Long> {
@Override
public boolean acceptsRow(Long rowObject) {
return true;
}
@Override
public boolean isSubFilterOf(TableFilter<?> tableFilter) {
return false;
}
}
private class PredicateTableFilter implements TableFilter<Long> {
private Predicate<Long> predicate;
PredicateTableFilter(Predicate<Long> predicate) {
this.predicate = predicate;
}
@Override
public boolean acceptsRow(Long rowObject) {
return predicate.test(rowObject);
}
@Override
public boolean isSubFilterOf(TableFilter<?> tableFilter) {
return false;
}
}
}

View File

@ -121,7 +121,7 @@ public class IncrementalThreadedTableTest extends AbstractThreadedTableTest {
@Override
protected TestThreadedTableModelListener createListener() {
return new TestIncrementalThreadedTableModelListener(spy);
return new TestIncrementalThreadedTableModelListener(model, spy);
}
//==================================================================================================

View File

@ -296,8 +296,8 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
@Test
public void testAddSendsEvent() {
waitForTableModel(model);
final AtomicReference<TableModelEvent> ref = new AtomicReference<>();
model.addTableModelListener(e -> ref.set(e));
AtomicReference<TableModelEvent> ref = new AtomicReference<>();
runSwing(() -> model.addTableModelListener(e -> ref.set(e)));
int newValue = model.getRowCount() + 1;
addItemToModel(newValue);

View File

@ -59,13 +59,12 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
buildFrame();
});
}
protected abstract TestDataKeyModel createTestModel();
protected TestThreadedTableModelListener createListener() {
return new TestThreadedTableModelListener();
return new TestThreadedTableModelListener(model);
}
@After
@ -257,8 +256,8 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
protected void assertRowCount(int expectedCount) {
int rowCount = model.getRowCount();
assertThat("Have different number of table rows than expected after filtering",
expectedCount, is(rowCount));
assertThat("Have different number of table rows than expected after filtering", rowCount,
is(expectedCount));
}
protected void assertNoRowsFilteredOut() {

View File

@ -19,8 +19,9 @@ import ghidra.docking.spy.SpyEventRecorder;
public class TestIncrementalThreadedTableModelListener extends TestThreadedTableModelListener {
TestIncrementalThreadedTableModelListener(SpyEventRecorder spy) {
super(spy);
TestIncrementalThreadedTableModelListener(ThreadedTableModel<?, ?> model,
SpyEventRecorder spy) {
super(model, spy);
}
@Override

View File

@ -25,46 +25,49 @@ public class TestThreadedTableModelListener implements ThreadedTableModelListene
private volatile boolean cancelled;
private SpyEventRecorder spy;
private ThreadedTableModel<?, ?> model;
public TestThreadedTableModelListener() {
this(new SpyEventRecorder("Listener Spy"));
public TestThreadedTableModelListener(ThreadedTableModel<?, ?> model) {
this(model, new SpyEventRecorder("Listener Spy"));
}
public TestThreadedTableModelListener(SpyEventRecorder spy) {
public TestThreadedTableModelListener(ThreadedTableModel<?, ?> model, SpyEventRecorder spy) {
this.spy = spy;
this.model = model;
}
void reset(ThreadedTableModel<?, ?> model) {
spy.record("listener - reset()");
void reset(ThreadedTableModel<?, ?> newModel) {
spy.record("Test - listener - reset()");
completed = cancelled = false;
}
boolean doneWork() {
spy.record("listener - doneWork()? " + (completed || cancelled) + " - complted? " +
spy.record("Test - listener - doneWork()? " + (completed || cancelled) + " - completed? " +
completed + "; cancelled? " + cancelled);
return completed || cancelled;
}
boolean startedWork() {
spy.record("listener - startedWork() - updating? " + updating);
spy.record("Test - listener - startedWork() - updating? " + updating);
return updating;
}
@Override
public void loadPending() {
spy.record("listener - loadPending()");
spy.record("Swing - listener - loadPending()");
pending = true;
}
@Override
public void loadingStarted() {
spy.record("listener - loadStarted()");
spy.record("Swing - listener - loadStarted()");
updating = true;
}
@Override
public void loadingFinished(boolean wasCancelled) {
spy.record("listener - loadingFinished() - cancelled? " + wasCancelled);
spy.record("Swing - listener - loadingFinished() - cancelled? " + wasCancelled +
"; size: " + model.getRowCount());
cancelled = wasCancelled;
completed = !cancelled;
}

View File

@ -17,6 +17,7 @@ package ghidra.docking.spy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.time.FastDateFormat;
@ -33,20 +34,37 @@ public class SpyEventRecorder {
private String recorderName;
private List<SpyEvent> events = new ArrayList<>();
private AtomicBoolean buffered = new AtomicBoolean(true);
public SpyEventRecorder(String recorderName) {
this.recorderName = recorderName;
}
public void setBuffered(boolean buffered) {
this.buffered.set(buffered);
}
// synchronized because we spy on multiple threads (like Test and Swing)
public synchronized void record(String message) {
SpyEvent event = new SpyEvent(message);
events.add(event);
if (buffered.get()) {
events.add(event);
}
else {
// System.err intentional here for aesthetics
System.err.println(event.toString(0));
}
}
private synchronized String eventsToString() {
int size = events.size();
int length = Integer.toString(size).length();
StringBuilder buffy = new StringBuilder("Recorded Events - " + recorderName + '\n');
for (SpyEvent event : events) {
buffy.append(event.toString()).append('\n');
buffy.append(event.toString(length)).append('\n');
}
return buffy.toString();
}
@ -63,6 +81,7 @@ public class SpyEventRecorder {
private class SpyEvent {
private static final String PADDING = " ";
private FastDateFormat dateFormat = FastDateFormat.getInstance("'T'HH:mm:ss:SSS");
private int id;
@ -74,9 +93,13 @@ public class SpyEventRecorder {
this.id = ++globalId;
}
@Override
public String toString() {
return "(" + id + ") " + dateFormat.format(time) + " " + message;
String toString(int idPad) {
int myLength = Integer.toString(id).length();
int delta = Math.max(0, idPad - myLength);
String pad = PADDING.substring(0, delta);
return "(" + id + ") " + pad + dateFormat.format(time) + " " + message;
}
}
}

View File

@ -96,7 +96,7 @@ public class ResourceManager {
return is;
}
URL url = getResource(testSearchPaths, filename);
URL url = getResource(getTestSearchPaths(), filename);
if (url == null) {
return null;
}