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

@ -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;