From 445c7ca03eeee00d48d3cfda856055e15fd0c8f8 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Tue, 7 May 2019 18:56:51 -0400 Subject: [PATCH] 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 --- .../app/plugin/core/symtable/SymbolPanel.java | 39 +++- .../markuptable/VTMarkupItemsTableModel.java | 73 ++++++-- .../gui/util/AbstractVTMatchTableModel.java | 47 ++++- .../filter/AbstractPatternTextFilter.java | 42 ++++- .../widgets/filter/ContainsTextFilter.java | 7 +- .../widgets/filter/InvertedTextFilter.java | 28 +++ .../filter/MatchesExactlyTextFilter.java | 7 +- .../filter/MatchesPatternTextFilter.java | 45 ++++- .../widgets/filter/StartsWithTextFilter.java | 7 +- .../table/AbstractSortedTableModel.java | 3 - .../widgets/table/CombinedTableFilter.java | 31 +++- .../table/DefaultRowFilterTransformer.java | 42 ++++- .../table/DefaultTableTextFilterFactory.java | 12 +- .../widgets/table/InvertedTableFilter.java | 29 +++ .../table/MultiTextFilterTableFilter.java | 42 ++++- .../widgets/table/RowObjectFilterModel.java | 6 - .../widgets/table/TableTextFilter.java | 33 ++++ .../columnfilter/ColumnBasedTableFilter.java | 4 - .../widgets/table/threaded/AddRemoveJob.java | 18 +- .../table/threaded/NullTableFilter.java | 19 ++ .../widgets/table/threaded/SortJob.java | 14 +- .../widgets/table/threaded/TableData.java | 25 ++- .../table/threaded/TableUpdateJob.java | 37 ++-- .../table/threaded/ThreadedTableModel.java | 6 +- .../DefaultThreadedTableFilterTest.java | 171 ++++++++++++++++-- .../IncrementalThreadedTableTest.java | 2 +- .../table/threaded/ThreadedTableTest.java | 4 +- .../threaded/AbstractThreadedTableTest.java | 7 +- ...IncrementalThreadedTableModelListener.java | 5 +- .../TestThreadedTableModelListener.java | 23 ++- .../ghidra/docking/spy/SpyEventRecorder.java | 33 +++- .../main/java/resources/ResourceManager.java | 2 +- 32 files changed, 720 insertions(+), 143 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolPanel.java index dcfb12b93f..7d7691ed7b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolPanel.java @@ -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; + } } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/markuptable/VTMarkupItemsTableModel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/markuptable/VTMarkupItemsTableModel.java index bb530346d3..117880b2aa 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/markuptable/VTMarkupItemsTableModel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/markuptable/VTMarkupItemsTableModel.java @@ -243,6 +243,43 @@ public class VTMarkupItemsTableModel extends AddressBasedTableModel sourceCellRenderer = new AbstractGColumnRenderer() { + private GColumnRenderer sourceCellRenderer = new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -543,30 +580,28 @@ public class VTMarkupItemsTableModel extends AddressBasedTableModel { - private GColumnRenderer isInDBCellRenderer = - new AbstractGColumnRenderer() { - @Override - public Component getTableCellRendererComponent(GTableCellRenderingData data) { + private GColumnRenderer 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() { diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractVTMatchTableModel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractVTMatchTableModel.java index 395d01b5ab..2d85e96578 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractVTMatchTableModel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractVTMatchTableModel.java @@ -203,6 +203,41 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel { @@ -397,7 +432,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel renderer = new AbstractGColumnRenderer() { + private GColumnRenderer renderer = new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -457,7 +492,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel renderer = new AbstractGColumnRenderer() { + private GColumnRenderer renderer = new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -550,7 +585,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel labelCellRenderer = - new AbstractGColumnRenderer() { + new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -681,7 +716,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel addressCellRenderer = - new AbstractGColumnRenderer() { + new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -788,7 +823,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel labelCellRenderer = - new AbstractGColumnRenderer() { + new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { @@ -921,7 +956,7 @@ public abstract class AbstractVTMatchTableModel extends AddressBasedTableModel addressCellRenderer = - new AbstractGColumnRenderer() { + new AbstractGColumnRenderer<>() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/AbstractPatternTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/AbstractPatternTextFilter.java index 3e49873203..a5025b2523 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/AbstractPatternTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/AbstractPatternTextFilter.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/ContainsTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/ContainsTextFilter.java index cfe3128b8f..1cd9fe964e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/ContainsTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/ContainsTextFilter.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/InvertedTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/InvertedTextFilter.java index 9630baf1c4..9242487ad0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/InvertedTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/InvertedTextFilter.java @@ -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; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesExactlyTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesExactlyTextFilter.java index e322206580..bc6e8c2415 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesExactlyTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesExactlyTextFilter.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesPatternTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesPatternTextFilter.java index d4734f82f8..ba18333829 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesPatternTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/MatchesPatternTextFilter.java @@ -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; + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/StartsWithTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/StartsWithTextFilter.java index 8c8eb22713..3463140415 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/StartsWithTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/StartsWithTextFilter.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractSortedTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractSortedTableModel.java index de66c700b3..030e31d6c4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractSortedTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractSortedTableModel.java @@ -232,9 +232,6 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel * fact that the data searched is retrieved from {@link #getModelData()}, which may be * filtered. * - *

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 */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/CombinedTableFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/CombinedTableFilter.java index 005a818f46..923d0bf2b6 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/CombinedTableFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/CombinedTableFilter.java @@ -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 implements TableFilter { } 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; + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultRowFilterTransformer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultRowFilterTransformer.java index 2c36a1598c..6671106bd1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultRowFilterTransformer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultRowFilterTransformer.java @@ -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 implements RowFilterTransformer { - private List columnData = new ArrayList(); + private List columnData = new ArrayList<>(); private TableColumnModel columnModel; private final RowObjectTableModel model; @@ -129,4 +128,41 @@ public class DefaultRowFilterTransformer implements RowFilterTransfo (GColumnRenderer) 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; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultTableTextFilterFactory.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultTableTextFilterFactory.java index 3c405c19bc..29485cdba9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultTableTextFilterFactory.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DefaultTableTextFilterFactory.java @@ -20,8 +20,8 @@ import java.util.List; import docking.widgets.filter.*; -public class DefaultTableTextFilterFactory implements - TableTextFilterFactory { +public class DefaultTableTextFilterFactory + implements TableTextFilterFactory { private final TextFilterFactory textFilterFactory; private final boolean inverted; @@ -40,7 +40,7 @@ public class DefaultTableTextFilterFactory implements TableFilter tableFilter = getBaseFilter(text, transformer); if (inverted && tableFilter != null) { - tableFilter = new InvertedTableFilter(tableFilter); + tableFilter = new InvertedTableFilter<>(tableFilter); } return tableFilter; } @@ -55,14 +55,14 @@ public class DefaultTableTextFilterFactory implements if (textFilter == null) { return null; } - return new TableTextFilter(textFilter, transformer); + return new TableTextFilter<>(textFilter, transformer); } private TableFilter getMultiWordTableFilter(String text, RowFilterTransformer transformer) { - List filters = new ArrayList(); + List 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 implements filters.add(textFilter); } } - return new MultiTextFilterTableFilter(text, filters, transformer, + return new MultiTextFilterTableFilter<>(filters, transformer, filterOptions.getMultitermEvaluationMode()); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/InvertedTableFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/InvertedTableFilter.java index 17ec8539e1..25347c05d0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/InvertedTableFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/InvertedTableFilter.java @@ -15,6 +15,8 @@ */ package docking.widgets.table; +import java.util.Objects; + public class InvertedTableFilter implements TableFilter { private final TableFilter filter; @@ -34,4 +36,31 @@ public class InvertedTableFilter implements TableFilter 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; + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MultiTextFilterTableFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MultiTextFilterTableFilter.java index 78f6ec3500..1ad2ba47f1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MultiTextFilterTableFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MultiTextFilterTableFilter.java @@ -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 implements TableFilter { private final List filters; - private final String text; private final RowFilterTransformer transformer; private final MultitermEvaluationMode evalMode; - public MultiTextFilterTableFilter(String text, List filters, + public MultiTextFilterTableFilter(List filters, RowFilterTransformer transformer, MultitermEvaluationMode evalMode) { - this.text = text; this.filters = filters; this.transformer = transformer; this.evalMode = evalMode; @@ -90,4 +89,41 @@ public class MultiTextFilterTableFilter implements TableFilter 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; + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/RowObjectFilterModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/RowObjectFilterModel.java index 178f01c20c..734bc49be6 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/RowObjectFilterModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/RowObjectFilterModel.java @@ -64,9 +64,6 @@ public interface RowObjectFilterModel extends RowObjectTableModelThis 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. * - *

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 extends RowObjectTableModelThis 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. * - *

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 */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableTextFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableTextFilter.java index 5b7d4df07c..781725d954 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableTextFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableTextFilter.java @@ -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 implements TableFilter { 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() + "'"; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnBasedTableFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnBasedTableFilter.java index da3cc6b624..b3d50e341f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnBasedTableFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnBasedTableFilter.java @@ -318,10 +318,6 @@ public class ColumnBasedTableFilter implements TableFilter { list.add(constraintSet); } - public boolean isEmpty() { - return list.isEmpty(); - } - boolean acceptsRow(R rowObject) { for (ColumnConstraintSet constraintSet : list) { if (!constraintSet.accepts(rowObject, tableFilterContext)) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/AddRemoveJob.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/AddRemoveJob.java index 4cd3d925b7..6dcb2bffc5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/AddRemoveJob.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/AddRemoveJob.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. @@ -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 extends TableUpdateJob { AddRemoveJob(ThreadedTableModel model, List> 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; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/NullTableFilter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/NullTableFilter.java index 605aba265b..0e85d2cc36 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/NullTableFilter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/NullTableFilter.java @@ -38,4 +38,23 @@ public class NullTableFilter implements TableFilter { // 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(); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/SortJob.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/SortJob.java index f6505cc9a4..3c40a297a9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/SortJob.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/SortJob.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. @@ -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 extends TableUpdateJob { @@ -30,10 +29,15 @@ public class SortJob extends TableUpdateJob { @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; } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java index 11f19e548f..00cc7c5160 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableData.java @@ -83,9 +83,13 @@ public class TableData implements Iterable { } TableData copy() { + return copy(source); + } + + TableData copy(TableData newSource) { List dataCopy = new ArrayList<>(data); TableData 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 implements Iterable { } /** - * 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 implements Iterable { * @return true if the source data nor the filter are different that what is used by this object. */ boolean matchesFilter(TableFilter 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 implements Iterable { 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. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java index 13016de9ef..e4d0e72447 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/TableUpdateJob.java @@ -166,7 +166,8 @@ public class TableUpdateJob { * * @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 item, int maxAddRemoveCount) { if (currentState != NOT_RUNNING) { @@ -219,17 +220,17 @@ public class TableUpdateJob { /** * 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: *

    *
  • 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. *
  • 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. *
  • 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. *
* @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 { 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 { /** 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 { List list = filterSourceData.getData(); List 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 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 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(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java index af78fb31e5..12f451ff6c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java @@ -486,7 +486,11 @@ public abstract class ThreadedTableModel 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; diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/DefaultThreadedTableFilterTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/DefaultThreadedTableFilterTest.java index 9058fb2598..8349871923 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/DefaultThreadedTableFilterTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/DefaultThreadedTableFilterTest.java @@ -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 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 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 nonTextFilter) { + + // the row objects are Long values that are 0-based one-up index values + DefaultRowFilterTransformer transformer = + new DefaultRowFilterTransformer<>(model, table.getColumnModel()); + + TextFilter allPassesFilter = new EmptyTextFilter(); + spyFilter = new SpyTextFilter<>(allPassesFilter, transformer, recorder); + + CombinedTableFilter 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 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 { + + @Override + public boolean acceptsRow(Long rowObject) { + return true; + } + + @Override + public boolean isSubFilterOf(TableFilter tableFilter) { + return false; + } + } + + private class PredicateTableFilter implements TableFilter { + + private Predicate predicate; + + PredicateTableFilter(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean acceptsRow(Long rowObject) { + return predicate.test(rowObject); + } + + @Override + public boolean isSubFilterOf(TableFilter tableFilter) { + return false; + } + } } diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/IncrementalThreadedTableTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/IncrementalThreadedTableTest.java index c17fa91343..753f57a90c 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/IncrementalThreadedTableTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/IncrementalThreadedTableTest.java @@ -121,7 +121,7 @@ public class IncrementalThreadedTableTest extends AbstractThreadedTableTest { @Override protected TestThreadedTableModelListener createListener() { - return new TestIncrementalThreadedTableModelListener(spy); + return new TestIncrementalThreadedTableModelListener(model, spy); } //================================================================================================== diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java index 40e01949b8..da907dd69c 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java @@ -296,8 +296,8 @@ public class ThreadedTableTest extends AbstractThreadedTableTest { @Test public void testAddSendsEvent() { waitForTableModel(model); - final AtomicReference ref = new AtomicReference<>(); - model.addTableModelListener(e -> ref.set(e)); + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> model.addTableModelListener(e -> ref.set(e))); int newValue = model.getRowCount() + 1; addItemToModel(newValue); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/AbstractThreadedTableTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/AbstractThreadedTableTest.java index a72c59f4a3..94b901796b 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/AbstractThreadedTableTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/AbstractThreadedTableTest.java @@ -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() { diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestIncrementalThreadedTableModelListener.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestIncrementalThreadedTableModelListener.java index 4b71da7495..bf1bb81c29 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestIncrementalThreadedTableModelListener.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestIncrementalThreadedTableModelListener.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestThreadedTableModelListener.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestThreadedTableModelListener.java index 7d44b92fb2..59ee179b8e 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestThreadedTableModelListener.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/table/threaded/TestThreadedTableModelListener.java @@ -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; } diff --git a/Ghidra/Framework/Docking/src/test/java/ghidra/docking/spy/SpyEventRecorder.java b/Ghidra/Framework/Docking/src/test/java/ghidra/docking/spy/SpyEventRecorder.java index 9ac24b422b..e08d8548dc 100644 --- a/Ghidra/Framework/Docking/src/test/java/ghidra/docking/spy/SpyEventRecorder.java +++ b/Ghidra/Framework/Docking/src/test/java/ghidra/docking/spy/SpyEventRecorder.java @@ -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 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; } } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java index 3024abe69f..fcf34b8264 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java @@ -96,7 +96,7 @@ public class ResourceManager { return is; } - URL url = getResource(testSearchPaths, filename); + URL url = getResource(getTestSearchPaths(), filename); if (url == null) { return null; }