GT-2763 - Tables - Added the ability to turn off sorting on tables

This commit is contained in:
dragonmacher 2019-05-02 18:09:17 -04:00
parent 538cbc1226
commit 4a8144c288
29 changed files with 384 additions and 146 deletions

View File

@ -112,7 +112,7 @@ class CommentWindowProvider extends ComponentProviderAdapter {
commentModel.addTableModelListener(e -> {
int rowCount = commentModel.getRowCount();
int unfilteredCount = commentModel.getUnfilteredCount();
int unfilteredCount = commentModel.getUnfilteredRowCount();
StringBuilder buffy = new StringBuilder();

View File

@ -118,7 +118,7 @@ class DataWindowProvider extends ComponentProviderAdapter {
dataModel.addTableModelListener(e -> {
int rowCount = dataModel.getRowCount();
int unfilteredCount = dataModel.getUnfilteredCount();
int unfilteredCount = dataModel.getUnfilteredRowCount();
StringBuilder buffy = new StringBuilder();

View File

@ -133,7 +133,7 @@ public class FunctionWindowProvider extends ComponentProviderAdapter {
functionModel.addTableModelListener(e -> {
int rowCount = functionModel.getRowCount();
int unfilteredCount = functionModel.getUnfilteredCount();
int unfilteredCount = functionModel.getUnfilteredRowCount();
StringBuilder buffy = new StringBuilder();

View File

@ -134,7 +134,7 @@ public class StringTableProvider extends ComponentProviderAdapter implements Dom
StringBuilder builder = new StringBuilder();
int rowCount = stringModel.getRowCount();
int unfilteredCount = stringModel.getUnfilteredCount();
int unfilteredCount = stringModel.getUnfilteredRowCount();
builder.append(rowCount);
builder.append(" items");

View File

@ -124,7 +124,7 @@ public class ViewStringsProvider extends ComponentProviderAdapter {
stringModel.addTableModelListener(e -> {
int rowCount = stringModel.getRowCount();
int unfilteredCount = stringModel.getUnfilteredCount();
int unfilteredCount = stringModel.getUnfilteredRowCount();
setSubTitle("" + rowCount + " items" +
(rowCount != unfilteredCount ? " (of " + unfilteredCount + ")" : ""));

View File

@ -152,7 +152,7 @@ class SymbolProvider extends ComponentProviderAdapter {
private String generateSubTitle() {
SymbolFilter filter = symbolKeyModel.getFilter();
int rowCount = symbolKeyModel.getRowCount();
int unfilteredCount = symbolKeyModel.getUnfilteredCount();
int unfilteredCount = symbolKeyModel.getUnfilteredRowCount();
if (rowCount != unfilteredCount) {
return " (Text filter matched " + rowCount + " of " + unfilteredCount + " symbols)";

View File

@ -65,18 +65,15 @@ public abstract class AddressBasedTableModel<ROW_TYPE> extends GhidraProgramTabl
ROW_TYPE rowObject = filteredData.get(row);
DynamicTableColumn<ROW_TYPE, ?, ?> tableColumn = getColumn(column);
if (tableColumn != null) {
if (tableColumn instanceof ProgramLocationTableColumn<?, ?>) {
if (tableColumn instanceof ProgramLocationTableColumn<?, ?>) {
@SuppressWarnings("unchecked")
// we checked
ProgramLocationTableColumn<ROW_TYPE, ?> programField =
(ProgramLocationTableColumn<ROW_TYPE, ?>) tableColumn;
ProgramLocation loc = programField.getProgramLocation(rowObject,
getColumnSettings(column), getProgram(), serviceProvider);
if (loc != null) {
return loc;
}
@SuppressWarnings("unchecked") // we checked
ProgramLocationTableColumn<ROW_TYPE, ?> programField =
(ProgramLocationTableColumn<ROW_TYPE, ?>) tableColumn;
ProgramLocation loc = programField.getProgramLocation(rowObject,
getColumnSettings(column), getProgram(), serviceProvider);
if (loc != null) {
return loc;
}
}

View File

@ -795,7 +795,7 @@ public class VTFunctionAssociationProvider extends ComponentProviderAdapter
private void getTableFilterString(String tableName, ThreadedTableModel<?, ?> model,
StringBuffer buffy) {
int filteredCount = model.getRowCount();
int unfilteredCount = model.getUnfilteredCount();
int unfilteredCount = model.getUnfilteredRowCount();
buffy.append(tableName).append(" - ").append(filteredCount).append(" functions");
if (filteredCount != unfilteredCount) {

View File

@ -263,7 +263,7 @@ public class VTImpliedMatchesTableProvider extends ComponentProviderAdapter
impliedMatchTableModel.addTableModelListener(e -> {
int filteredCount = impliedMatchTableModel.getRowCount();
int unfilteredCount = impliedMatchTableModel.getUnfilteredCount();
int unfilteredCount = impliedMatchTableModel.getUnfilteredRowCount();
String sessionName = controller.getVersionTrackingSessionName();
StringBuffer buffy = new StringBuffer();

View File

@ -321,14 +321,14 @@ public class VTMarkupItemsTableProvider extends ComponentProviderAdapter
markupItemsTableModel = new VTMarkupItemsTableModel(controller);
markupItemsTableModel.addTableModelListener(e -> {
int filteredCount = markupItemsTableModel.getRowCount();
int unfilteredCount = markupItemsTableModel.getUnfilteredCount();
int unfilteredCount = markupItemsTableModel.getUnfilteredRowCount();
String sessionName = controller.getVersionTrackingSessionName();
StringBuffer buffy = new StringBuffer();
buffy.append("[Session: ").append(sessionName).append("] ");
buffy.append('-').append(markupItemsTableModel.getRowCount()).append(" markup items");
if (filteredCount != unfilteredCount) {
buffy.append(" (of ").append(markupItemsTableModel.getUnfilteredCount()).append(
buffy.append(" (of ").append(markupItemsTableModel.getUnfilteredRowCount()).append(
')');
}
@ -848,7 +848,7 @@ public class VTMarkupItemsTableProvider extends ComponentProviderAdapter
if (filtered) {
int filteredCount = markupItemsTableModel.getRowCount();
int unfilteredCount = markupItemsTableModel.getUnfilteredCount();
int unfilteredCount = markupItemsTableModel.getUnfilteredRowCount();
int filteredOutCount = unfilteredCount - filteredCount;
ancillaryFilterButton.setToolTipText(
"More Filters - " + filteredOutCount + " item(s) hidden");

View File

@ -181,7 +181,7 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
if (filtered) {
int filteredCount = matchesTableModel.getRowCount();
int unfilteredCount = matchesTableModel.getUnfilteredCount();
int unfilteredCount = matchesTableModel.getUnfilteredRowCount();
int filteredOutCount = unfilteredCount - filteredCount;
ancillaryFilterButton.setToolTipText(
"More Filters - " + filteredOutCount + " item(s) hidden");
@ -231,7 +231,7 @@ public class VTMatchTableProvider extends ComponentProviderAdapter
matchesTableModel = new VTMatchTableModel(controller);
matchesTableModel.addTableModelListener(e -> {
int filteredCount = matchesTableModel.getRowCount();
int unfilteredCount = matchesTableModel.getUnfilteredCount();
int unfilteredCount = matchesTableModel.getUnfilteredRowCount();
String sessionName = controller.getVersionTrackingSessionName();
StringBuffer buffy = new StringBuffer();

View File

@ -93,13 +93,13 @@ public abstract class AbstractGTableModel<T> extends AbstractTableModel
}
/**
* A convenience method for subclasses to quickly/efficiently search for the index of a given
* A convenience method to search for the index of a given
* row object <b>that is visible in the GUI</b>. The <i>visible</i> limitation is due to the
* fact that the data searched is retrieved from {@link #getModelData()}, which may be
* filtered.
* <p>
* If a need for access to all of the data is required in the future, then an overloaded
* version of this method should be created that takes the data to be searched.
*
* <p>Note: this operation is O(n). For quick lookups, consider using the sorted version
* of this class.
*
* @param rowObject The object for which to search.
* @return the index of the item in the data returned by
@ -108,6 +108,14 @@ public abstract class AbstractGTableModel<T> extends AbstractTableModel
return getIndexForRowObject(rowObject, getModelData());
}
/**
* Returns the index for the given object in the given list; -1 when the item is not in
* the list.
*
* @param rowObject the item
* @param data the data
* @return the index
*/
protected int getIndexForRowObject(T rowObject, List<T> data) {
return data.indexOf(rowObject);
}

View File

@ -17,10 +17,10 @@ package docking.widgets.table;
import java.util.*;
import javax.swing.SwingUtilities;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableModel;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
@ -31,15 +31,21 @@ import ghidra.util.datastruct.WeakSet;
* <p>
* In order to define custom comparators for a column, simply override
* {@link #createSortComparator(int)}. Otherwise, a default comparator will be created for you.
*
* <p>Note on sorting: it is possible that the user can disable sorting by de-selecting all
* sorted columns. This can also be achieved programmatically by calling
* {@link #setTableSortState(TableSortState)} with a value of
* {@link TableSortState#createUnsortedSortState()}.
*
* @param <T> The row type upon which the table is based
*/
public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
implements SortedTableModel {
private static final long serialVersionUID = 1L;
private final Comparator<T> NO_SORT_COMPARATOR = (o1, o2) -> 0;
private TableSortState pendingSortState;
private TableSortState sortState;
private TableSortState sortState = TableSortState.createUnsortedSortState();
private boolean isSortPending;
protected boolean hasEverSorted;
@ -58,6 +64,10 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
protected void setDefaultTableSortState(TableSortState defaultSortState) {
sortState = defaultSortState;
if (sortState == null) {
sortState = TableSortState.createUnsortedSortState();
}
}
@Override
@ -81,14 +91,17 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
/**
* Returns the index of the given row object in this model; -1 if the model does not contain
* the given object.
* the given object.
*
* <p>Warning: if the this model has no sort applied, then performance will be O(n). If
* sorted, then performance is O(log n). You can call {@link #isSorted()} to know when
* this will happen.
*/
@Override
public int getRowIndex(T rowObject) {
if (rowObject == null) {
return -1;
}
return getIndexForRowObject(rowObject);
}
@ -121,7 +134,7 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
}
@Override
public void setTableSortState(final TableSortState newSortState) {
public void setTableSortState(TableSortState newSortState) {
if (!isValidSortState(newSortState)) {
// if the user calls this method with an invalid value, then let them know!
@ -165,7 +178,8 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
isSortPending = true;
pendingSortState = newSortState;
SwingUtilities.invokeLater(() -> sort(getModelData(), createSortingContext(newSortState)));
SystemUtilities.runSwingLater(
() -> sort(getModelData(), createSortingContext(newSortState)));
}
public TableSortState getPendingSortState() {
@ -176,6 +190,10 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
return isSortPending;
}
public boolean isSorted() {
return !isSortPending && !sortState.isUnsorted();
}
protected TableSortingContext<T> createSortingContext(TableSortState newSortState) {
return new TableSortingContext<>(newSortState, getComparatorChain(newSortState));
}
@ -205,7 +223,7 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
hasEverSorted = true;
isSortPending = true;
pendingSortState = sortState;
SwingUtilities.invokeLater(() -> sort(getModelData(), createSortingContext(sortState)));
SystemUtilities.runSwingLater(() -> sort(getModelData(), createSortingContext(sortState)));
}
/**
@ -213,9 +231,9 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
* row object <b>that is visible in the GUI</b>. The <i>visible</i> limitation is due to the
* fact that the data searched is retrieved from {@link #getModelData()}, which may be
* filtered.
* <p>
* If a need for access to all of the data is required in the future, then an overloaded
* version of this method should be created that takes the data to be searched.
*
* <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
@ -225,10 +243,27 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
return getIndexForRowObject(rowObject, getModelData());
}
/**
* Returns the index for the given object in the given list
*
* @param rowObject the item
* @param data the data
* @return the index
*/
@Override
protected int getIndexForRowObject(T rowObject, List<T> data) {
Comparator<T> comparator = getComparatorChain(sortState);
return Collections.binarySearch(data, rowObject, comparator);
if (isSorted()) {
Comparator<T> comparator = getComparatorChain(sortState);
return Collections.binarySearch(data, rowObject, comparator);
}
for (int i = 0; i < data.size(); i++) {
T t = data.get(i);
if (rowObject.equals(t)) {
return i;
}
}
return -1;
}
/**
@ -242,6 +277,14 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
* comparator for sorting, etc...).
*/
protected void sort(List<T> data, TableSortingContext<T> sortingContext) {
if (sortingContext.isUnsorted()) {
// this is the 'no sort' state
sortCompleted(sortingContext);
notifyModelSorted(false);
return;
}
hasEverSorted = true; // signal that we have sorted at least one time
Collections.sort(data, sortingContext.getComparator());
sortCompleted(sortingContext);
@ -315,6 +358,11 @@ public abstract class AbstractSortedTableModel<T> extends AbstractGTableModel<T>
* data is sorted).
*/
private Comparator<T> getComparatorChain(TableSortState newSortState) {
if (newSortState.isUnsorted()) {
return NO_SORT_COMPARATOR;
}
ComparatorLink comparatorLink = new ComparatorLink();
for (ColumnSortState columnSortState : newSortState) {
Comparator<T> nextComparator = getComparator(columnSortState);

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.
@ -29,7 +28,7 @@ public class ColumnSortState {
private static final String XML_SORT_ORDER = "SORT_ORDER";
public enum SortDirection {
ASCENDING, DESCENDING;
ASCENDING, DESCENDING, UNSORTED;
public boolean isAscending() {
return this == ASCENDING;
@ -82,9 +81,8 @@ public class ColumnSortState {
}
public ColumnSortState createFlipState() {
ColumnSortState newSortState =
new ColumnSortState(columnModelIndex, isAscending() ? SortDirection.DESCENDING
: SortDirection.ASCENDING, sortOrder_OneBased);
ColumnSortState newSortState = new ColumnSortState(columnModelIndex,
isAscending() ? SortDirection.DESCENDING : SortDirection.ASCENDING, sortOrder_OneBased);
return newSortState;
}

View File

@ -56,7 +56,35 @@ public interface RowObjectFilterModel<ROW_OBJECT> extends RowObjectTableModel<RO
public int getViewRow(int modelRow);
/**
* Returns the view index of the given item. When filtered, this is the index is the smaller,
* visible set of data; when unfiltered, this index is the same as that returned by
* {@link #getModelIndex(Object)}.
*
* <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
*/
public int getViewIndex(ROW_OBJECT t);
/**
* Returns the model index of the given item. When filtered, this is the index is the larger,
* set of data; when unfiltered, this index is the same as that returned by
* {@link #getModelIndex(Object)}.
*
* <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
*/
public int getModelIndex(ROW_OBJECT t);
}

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.
@ -32,7 +31,15 @@ public class TableSortState implements Iterable<ColumnSortState> {
private static final String XML_TABLE_SORT_STATE = "TABLE_SORT_STATE";
private List<ColumnSortState> columnSortStates = new ArrayList<ColumnSortState>();
private List<ColumnSortState> columnSortStates = new ArrayList<>();
/**
* Creates a sort state that represents being unsorted
* @return a sort state that represents being unsorted
*/
public static TableSortState createUnsortedSortState() {
return new TableSortState();
}
/**
* Creates a sort state with the given column as the sorted column (sorted ascending).
@ -64,8 +71,8 @@ public class TableSortState implements Iterable<ColumnSortState> {
}
public TableSortState(List<ColumnSortState> sortStates) {
List<Integer> sortOrders = new ArrayList<Integer>();
List<Integer> columnIndices = new ArrayList<Integer>();
List<Integer> sortOrders = new ArrayList<>();
List<Integer> columnIndices = new ArrayList<>();
for (ColumnSortState state : sortStates) {
int sortOrder = state.getSortOrder();
if (sortOrders.contains(sortOrder)) {
@ -84,7 +91,7 @@ public class TableSortState implements Iterable<ColumnSortState> {
columnIndices.add(columnModelIndex);
}
this.columnSortStates = new ArrayList<ColumnSortState>(sortStates);
this.columnSortStates = new ArrayList<>(sortStates);
}
public TableSortState(ColumnSortState columnSortState) {
@ -100,6 +107,10 @@ public class TableSortState implements Iterable<ColumnSortState> {
return columnSortStates.size();
}
public boolean isUnsorted() {
return columnSortStates.isEmpty();
}
public ColumnSortState getColumnSortState(int columnIndex) {
for (ColumnSortState sortState : columnSortStates) {
if (sortState.getColumnModelIndex() == columnIndex) {
@ -110,7 +121,7 @@ public class TableSortState implements Iterable<ColumnSortState> {
}
public List<ColumnSortState> getAllSortStates() {
return new ArrayList<ColumnSortState>(columnSortStates);
return new ArrayList<>(columnSortStates);
}
@Override
@ -140,7 +151,7 @@ public class TableSortState implements Iterable<ColumnSortState> {
Element sortStateElement = (Element) tableSortStateElementList.get(0);
List<?> children = sortStateElement.getChildren(ColumnSortState.XML_COLUMN_SORT_STATE);
List<ColumnSortState> columnStates = new ArrayList<ColumnSortState>(children.size());
List<ColumnSortState> columnStates = new ArrayList<>(children.size());
for (Object object : children) {
columnStates.add(ColumnSortState.restoreFromXML((Element) object));
}

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,7 @@
*/
package docking.widgets.table;
import java.util.Comparator;
import java.util.Iterator;
import java.util.*;
public class TableSortingContext<T> {
@ -25,8 +23,8 @@ public class TableSortingContext<T> {
private Comparator<T> comparator;
public TableSortingContext(TableSortState sortState, Comparator<T> comparator) {
this.sortState = sortState;
this.comparator = comparator;
this.sortState = Objects.requireNonNull(sortState);
this.comparator = Objects.requireNonNull(comparator);
}
public Comparator<T> getComparator() {
@ -37,6 +35,15 @@ public class TableSortingContext<T> {
return sortState;
}
/**
* Returns true if there are no columns marked as sorted, which represents a 'no sort' state
*
* @return true if there are no columns sorted
*/
public boolean isUnsorted() {
return sortState.isUnsorted();
}
public boolean isReverseOf(TableSortingContext<T> otherContext) {
int sortedColumnCount = sortState.getSortedColumnCount();
if (sortedColumnCount != 1) {

View File

@ -15,8 +15,6 @@
*/
package docking.widgets.table;
import java.awt.Toolkit;
import javax.swing.JTable;
import javax.swing.table.*;
@ -113,11 +111,17 @@ public class TableUtils {
TableSortStateEditor editor = new TableSortStateEditor(columnSortStates);
if (editor.isColumnSorted(modelColumnIndex)) {
// remove it. If there is only one, don't remove the last one
if (editor.getSortedColumnCount() == 1) {
Toolkit.getDefaultToolkit().beep();
return;
}
/*
Note: this code allows us to disable the 'unsorting' of a table via the UI
// remove it. If there is only one, don't remove the last one
if (editor.getSortedColumnCount() == 1) {
Toolkit.getDefaultToolkit().beep();
return;
}
*/
editor.removeSortedColumn(modelColumnIndex);
}
else {

View File

@ -104,7 +104,7 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
}
boolean isSorted() {
return sortContext != null;
return sortContext != null && !sortContext.isUnsorted();
}
void setSortContext(TableSortingContext<ROW_OBJECT> sortContext) {
@ -128,12 +128,24 @@ 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
* @param t the item
* @return the index
*/
int indexOf(ROW_OBJECT t) {
Comparator<ROW_OBJECT> comparator = sortContext.getComparator();
int index = Collections.binarySearch(data, t, comparator);
return index;
if (!sortContext.isUnsorted()) {
Comparator<ROW_OBJECT> comparator = sortContext.getComparator();
return Collections.binarySearch(data, t, comparator);
}
// brute force
for (int i = 0; i < data.size(); i++) {
ROW_OBJECT item = data.get(i);
if (t.equals(item)) {
return i;
}
}
return -1;
}
boolean remove(ROW_OBJECT o) {
@ -145,9 +157,9 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
}
/**
* Adds the new <tt>value</tt> to the data at the appropriate location based on the sort.
* @param value the row Object to insert.
* @param comparator the comparator to use to find the appropriate location to insert the new data.
* Adds the new <tt>value</tt> to the data at the appropriate location based on the sort
*
* @param value the row Object to insert
*/
void insert(ROW_OBJECT value) {
@ -160,10 +172,8 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
return; // this item is filtered out of this data
}
if (sortContext == null) {
//
// Not yet sorted; just add the item anywhere and it will get sorted later
//
if (!isSorted()) {
// Not yet sorted or intentionally unsorted; just add the item, it will get sorted later
data.add(value);
return;
}
@ -180,7 +190,7 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
// The search thinks the item is in the list because a compareTo() result of 0 was
// found. If the two objects are not equal(), then add the new value.
ROW_OBJECT existingValue = data.get(index);
if (!SystemUtilities.isEqual(value, existingValue)) {
if (!Objects.equals(value, existingValue)) {
data.add(index, value);
}
}
@ -310,4 +320,10 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
// of 'data', as that could be expensive.
return super.equals(obj);
}
@Override
final public int hashCode() {
// Made final to match equals()
return super.hashCode();
}
}

View File

@ -498,6 +498,11 @@ public class TableUpdateJob<T> {
}
private void doSortData(List<T> data) {
if (newSortContext.isUnsorted()) {
return;
}
int size = data.size();
monitor.setMessage("Sorting " + model.getName() + " (" + size + " rows)" + "...");
monitor.initialize(size);
@ -512,7 +517,7 @@ public class TableUpdateJob<T> {
//
// Usually the source data is sorted before any filter is applied. However, this is not
// the case when a load of new data is followed directly by a filter action. We rely on
// the source data being filtered in order to perform fast translations from the table's
// the source data being sorted in order to perform fast translations from the table's
// view to the table's model when it is filtered. Thus, make sure that any time we are
// sorting the filtered data, that the source data too is sorted.
//

View File

@ -27,7 +27,6 @@ import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.*;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
import ghidra.util.worker.Worker;
/**
@ -36,14 +35,14 @@ import ghidra.util.worker.Worker;
* You can optionally set this model to load data incrementally by passing the correct
* constructor argument. Note, if you make this model incremental, then you need to set an
* incremental task monitor in order to get feedback about loading
* (see {@link #setIncrementalTaskMonitor(TaskMonitor)). Alternatively, you can use
* (see {@link #setIncrementalTaskMonitor(TaskMonitor)}. Alternatively, you can use
* a {@link GThreadedTablePanel}, which will install the proper monitor for you.
*
* @param ROW_OBJECT the row object class for this table model.
* @param DATA_SOURCE the type of data that will be returned from {@link #getDataSource()}. This
* @param <ROW_OBJECT> the row object class for this table model.
* @param <DATA_SOURCE> the type of data that will be returned from {@link #getDataSource()}. This
* object will be given to the {@link DynamicTableColumn} objects used by this
* table model when
* {@link DynamicTableColumn#getValue(Object, generic.settings.Settings, Object, ServiceProvider)}
* {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)}
* is called.
*/
public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
@ -52,7 +51,7 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
private ThreadedTableModelUpdateMgr<ROW_OBJECT> updateManager;
private boolean loadIncrementally;
private TaskMonitor incrementalMonitor = TaskMonitorAdapter.DUMMY_MONITOR;
private TaskMonitor incrementalMonitor = TaskMonitor.DUMMY;
private ConcurrentListenerSet<ThreadedTableModelListener> listeners =
new ConcurrentListenerSet<>();
@ -157,8 +156,12 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
/**
* A package-level method. Subclasses should not call this.
* <p>
* This exists to handle whether this model should load incrementally.
*
* <p>This exists to handle whether this model should load incrementally.
*
* @param monitor the monitor
* @return the loaded data
* @throws CancelledException
*/
final List<ROW_OBJECT> load(TaskMonitor monitor) throws CancelledException {
if (loadIncrementally) {
@ -261,7 +264,7 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
*
* @param rowObject The object for which to search
* @return The index for the given object; a negative value if the object is not in the list
* @see #getIndexForRowObject(Object);
* @see #getIndexForRowObject(Object)
*/
protected int getUnfilteredIndexForRowObject(ROW_OBJECT rowObject) {
return getIndexForRowObject(rowObject, getUnfilteredData());
@ -327,6 +330,7 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
/**
* Returns the current sorting context, which is the next one to be applied, if a sort is
* pending; otherwise the current sorting context.
* @return the sort context
*/
TableSortingContext<ROW_OBJECT> getSortingContext() {
if (pendingSortContext != null) {
@ -365,8 +369,7 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
/**
* Override this to change how filtering is performed. This implementation will do nothing
* if a <tt>TableFilter</tt> has not been set via a call to {@link #setTableFilter(TableFilter)}.
* Also, no filtering will happen if there is no filter text set via a call to
* {@link #setFilterText(String)}.
*
*
* @param data The list of data to be filtered.
*
@ -629,10 +632,6 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
return filteredData.size();
}
public int getUnfilteredCount() {
return allData.size();
}
/**
* Given a row index for the raw (unfiltered) model, return the corresponding index in the
* view (filtered) model.
@ -643,11 +642,12 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
*/
@Override
public int getViewRow(int modelRow) {
if (getRowCount() == getUnfilteredCount()) {
int unfilteredCount = getUnfilteredRowCount();
if (getRowCount() == unfilteredCount) {
return modelRow; // same list; no need to translate values
}
if (modelRow >= allData.size()) {
if (modelRow >= unfilteredCount) {
return -1; // out-of-bounds request
}
@ -665,7 +665,7 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
*/
@Override
public int getModelRow(int viewRow) {
if (getRowCount() == getUnfilteredCount()) {
if (getRowCount() == getUnfilteredRowCount()) {
return viewRow; // same list; no need to translate values
}
@ -715,8 +715,10 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
/**
* Sets the update delay, which is how long the model should wait before updating, after
* a change has been made the data.
* a change has been made the data
*
* @param updateDelayMillis the new update delay
* @param maxUpdateDelayMillis the new max update delay; updates will not wait past this time
*/
void setUpdateDelay(int updateDelayMillis, int maxUpdateDelayMillis) {
this.minUpdateDelayMillis = updateDelayMillis;
@ -724,16 +726,11 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
updateManager.setUpdateDelay(updateDelayMillis, maxUpdateDelayMillis);
}
/**
* @see #setMinDelay(int)
*/
// see setUpdateDelay
long getMinDelay() {
return minUpdateDelayMillis;
}
/**
* @see #setMaxDelay(int)
*/
long getMaxDelay() {
return maxUpdateDelayMillis;
}

View File

@ -297,7 +297,8 @@ class ThreadedTableModelUpdateMgr<T> {
/**
* Sets the delay for the swing update manager.
* @param updateDelayMillis the new delay for the swing update manager.
* @param updateDelayMillis the new delay for the swing update manager
* @param maxUpdateDelayMillis the new max update delay; updates will not wait past this time
*/
void setUpdateDelay(int updateDelayMillis, int maxUpdateDelayMillis) {
updateManager.dispose();

View File

@ -32,7 +32,7 @@ import ghidra.docking.spy.SpyEventRecorder;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.task.TaskMonitor;
public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
public class DefaultThreadedTableFilterTest extends AbstractThreadedTableTest {
private SpyEventRecorder recorder = new SpyEventRecorder(getClass().getSimpleName());
private SpyTaskMonitor monitor = new SpyTaskMonitor();
@ -98,7 +98,7 @@ public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
@Test
public void testRefilterHappensAfterAddItem_ItemAddedPassesFilter() throws Exception {
int newRowIndex = model.getRowCount() + 1;
int newRowIndex = getRowCount() + 1;
filterOnRawColumnValue(newRowIndex);
resetSpies();
@ -114,7 +114,7 @@ public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
@Test
public void testRefilterHappensAfterRemoveAddItem_ItemAddedPassesFilter() throws Exception {
int newRowIndex = model.getRowCount() + 1;
int newRowIndex = getRowCount() + 1;
filterOnRawColumnValue(newRowIndex);
resetSpies();
@ -137,7 +137,7 @@ public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
@Test
public void testRefilterHappensAfterAdd_ItemAddedFailsFilter() throws Exception {
int newRowIndex = model.getRowCount() + 1;
int newRowIndex = getRowCount() + 1;
long nonMatchingFilter = 1;
filterOnRawColumnValue(nonMatchingFilter);
@ -380,18 +380,18 @@ public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
}
private void assertFilteredEntireModel() {
int allCount = model.getUnfilteredCount();
int allCount = getUnfilteredRowCount();
assertNumberOfItemsPassedThroughFilter(allCount);
}
private void assertTableContainsValue(long expected) {
List<Long> modelValues = model.getModelData();
List<Long> modelValues = getModelData();
assertTrue("Value not in the model--filtered out? - Expected " + expected + "; found " +
modelValues, modelValues.contains(expected));
}
private void assertTableDoesNotContainValue(long expected) {
List<Long> modelValues = model.getModelData();
List<Long> modelValues = getModelData();
assertFalse("Value in the model--should not be there - Value " + expected + "; found " +
modelValues, modelValues.contains(expected));
}
@ -464,9 +464,11 @@ public class ThreadedTableFilterTest extends AbstractThreadedTableTest {
spyFilter = new SpyTextFilter<>(textFilter, transformer, recorder);
CombinedTableFilter<Long> combinedFilter =
new CombinedTableFilter<Long>(spyFilter, secondFilter, null);
new CombinedTableFilter<>(spyFilter, secondFilter, null);
recorder.record("Before setting the new filter");
runSwing(() -> model.setTableFilter(combinedFilter));
recorder.record("\tafter setting filter");
waitForNotBusy();
waitForTableModel(model);

View File

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.table.threaded;
import org.junit.Before;
import docking.widgets.table.TableSortState;
public class NonSortedThreadedTableFilterTest extends DefaultThreadedTableFilterTest {
@Override
@Before
public void setUp() throws Exception {
super.setUp();
TableSortState sortState = TableSortState.createUnsortedSortState();
runSwing(() -> model.setTableSortState(sortState));
waitForTableModel(model);
}
}

View File

@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.table.threaded;
import org.junit.Before;
import docking.widgets.table.TableSortState;
public class NonSortedThreadedTableTest extends ThreadedTableTest {
@Override
@Before
public void setUp() throws Exception {
super.setUp();
TableSortState sortState = TableSortState.createUnsortedSortState();
runSwing(() -> model.setTableSortState(sortState));
waitForTableModel(model);
}
}

View File

@ -29,8 +29,7 @@ public class SpyTextFilter<T> extends TableTextFilter<T> {
private SpyEventRecorder recorder;
SpyTextFilter(TextFilter textFilter, RowFilterTransformer<T> transformer) {
super(textFilter, transformer);
this.recorder = new SpyEventRecorder("Stub");
this(textFilter, transformer, new SpyEventRecorder("Stub"));
}
SpyTextFilter(TextFilter textFilter, RowFilterTransformer<T> transformer,

View File

@ -45,7 +45,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
private final Pattern SORT_SIZE_PATTERN = Pattern.compile(".*\\((\\d+) rows\\).*");
private SpyEventRecorder recorder = new SpyEventRecorder(getClass().getSimpleName());
private SpyEventRecorder recorder = new SpyEventRecorder(testName.getMethodName());
private SpyTaskMonitor spyMonitor = new SpyTaskMonitor(recorder);
private SpyTextFilter<Long> spyFilter;
private SortListener spySortListener =
@ -102,6 +102,12 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
public void setIncrementalTaskMonitor(TaskMonitor monitor) {
// no! some of our tests use a spy monitor
}
@Override
public void setTableSortState(TableSortState newSortState) {
record("model.setTableSortState() - " + newSortState);
super.setTableSortState(newSortState);
}
});
return box[0];
}
@ -111,7 +117,6 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
//
// make sure the sort is on the filtered data and not *all* data
//
waitForNotBusy();
assertSortSize(12);
filter_ten();
@ -512,7 +517,6 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
//
// we need to use a model that loads slowly enough to trigger the pending panel to show
//
waitForNotBusy();// if we are working while adding, we will not get the pending notification
model.setDelayTimeBetweenAddingDataItemsWhileLoading(60000);
model.setUpdateDelay(100000000, 100000001);// make sure we don't update after repeated requests arrive
@ -534,15 +538,15 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
@Override
protected void sortByClick(int columnToClick, int modifiers) throws Exception {
recorder.record(
"Test." + testName + " - clicking column=" + columnToClick + "; modifiers=" + 0);
recorder.record("Test." + testName.getMethodName() + " - clicking column=" + columnToClick +
"; modifiers=" + 0);
super.sortByClick(columnToClick, modifiers);
}
@Override
protected void removeSortByClicking(int columnToClick) throws Exception {
recorder.record(
"Test." + testName + " - clicking column to remove sort - column=" + columnToClick);
recorder.record("Test." + testName.getMethodName() +
" - clicking column to remove sort - column=" + columnToClick);
super.removeSortByClicking(columnToClick);
}
@ -607,7 +611,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
private void toggleStringColumnSort() throws Exception {
Rectangle rect = header.getHeaderRect(TestDataKeyModel.STRING_COL);
testTableModelListener.reset(model);
resetBusyListener();
clickMouse(header, MouseEvent.BUTTON1, rect.x + 10, rect.y + 10, 1, 0);
waitForNotBusy();
waitForSwing();
@ -753,6 +757,8 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
}
private void clearFilter() throws Exception {
resetSpies();
resetBusyListener();
runSwing(() -> model.setTableFilter(null));
waitForNotBusy();
@ -782,6 +788,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
@Override
protected void doTestSorting(int columnIndex) throws Exception {
sortByNormalClicking(columnIndex);
SortedTableModel sortedModel = (SortedTableModel) table.getModel();
@ -829,7 +836,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
TextFilterFactory textFactory = options.getTextFilterFactory();
TextFilter textFilter = textFactory.getTextFilter(text);
testTableModelListener.reset(model);
resetBusyListener();
spyFilter = new SpyTextFilter<>(textFilter, transformer, recorder);
@ -859,6 +866,11 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
assertTrue("Table did not filter; requested filter on '" + text + "'", hasFiltered);
}
@Override
protected void record(String message) {
recorder.record("Test - " + message);
}
private void resetSpies() {
spyFilter.reset();
spyMonitor.clearMessages();

View File

@ -90,12 +90,12 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
}
protected void addItemToModel(long value) {
model.addObject(new Long(value));
model.addObject(Long.valueOf(value));
waitForTableModel(model);
}
protected void removeItemFromModel(int value) {
model.removeObject(new Long(value));
model.removeObject(Long.valueOf(value));
waitForTableModel(model);
}
@ -161,10 +161,20 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
SortedTableModel sortedModel = (SortedTableModel) table.getModel();
TableSortState sortState = getSortState(sortedModel);
ColumnSortState originalColumnSortState = sortState.iterator().next();
int currentSortedIndex = originalColumnSortState.getColumnModelIndex();
boolean checkSortDirection = (columnToClick == currentSortedIndex);
boolean isAscending = originalColumnSortState.isAscending();
record("sortByClick() - initial sort state: " + sortState);
int currentSortColunn = -1;
boolean isAscending = true;
boolean checkSortDirection = false;
if (!sortState.isUnsorted()) {
// check to see if the tests is clicking the same column twice (to change the
// sort direction)
ColumnSortState originalColumnSortState = sortState.iterator().next();
currentSortColunn = originalColumnSortState.getColumnModelIndex();
checkSortDirection = (columnToClick == currentSortColunn);
isAscending = originalColumnSortState.isAscending();
}
testTableModelListener.reset(model);
Rectangle rect = header.getHeaderRect(columnToClick);
@ -172,10 +182,14 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
waitForPostedSwingRunnables();
}
record("Clicking table at column " + columnToClick);
clickMouse(header, MouseEvent.BUTTON1, rect.x + 10, rect.y + 10, 1, modifiers);
waitForNotBusy();
record("\tafter click; table not busy");
sortState = getSortState(sortedModel);
record("Updated sort state: " + sortState);
ColumnSortState columnSortState = sortState.iterator().next();
int sortedIndex = columnSortState.getColumnModelIndex();
verifyColumnSorted(sortedIndex, sortState);
@ -188,10 +202,8 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
}
}
protected TableSortState getSortState(final SortedTableModel sortedModel) {
final TableSortState[] box = new TableSortState[1];
runSwing(() -> box[0] = sortedModel.getTableSortState());
return box[0];
protected TableSortState getSortState(SortedTableModel sortedModel) {
return runSwing(() -> sortedModel.getTableSortState());
}
protected void removeSortByClicking(int columnToClick) throws Exception {
@ -212,20 +224,35 @@ public abstract class AbstractThreadedTableTest extends AbstractDockingTest {
assertNotNull(columnSortState);
}
protected void resetBusyListener() {
testTableModelListener.reset(model);
}
protected void waitForNotBusy() {
sleep(50);
int nWaits = 0;
int maxWaits = 500;
while (!testTableModelListener.doneWork() && nWaits++ < maxWaits) {
sleep(50);
}
assertTrue("Timed-out waiting for table model to update.", nWaits < maxWaits);
waitForCondition(() -> testTableModelListener.doneWork(),
"Timed-out waiting for table model to update.");
waitForSwing();
}
protected void addLong(final long value) {
runSwing(() -> model.addObject(new Long(value)));
runSwing(() -> model.addObject(Long.valueOf(value)));
}
protected int getRowCount() {
return runSwing(() -> model.getRowCount());
}
protected int getUnfilteredRowCount() {
return runSwing(() -> model.getUnfilteredRowCount());
}
protected List<Long> getModelData() {
return runSwing(() -> model.getModelData());
}
protected void record(String message) {
// no-op for base class; subclasses know how to record debug
}
protected void assertRowCount(int expectedCount) {

View File

@ -43,18 +43,27 @@ public class SpyEventRecorder {
events.add(event);
}
// synchronized because we spy on multiple threads (like Test and Swing)
public synchronized void dumpEvents() {
private synchronized String eventsToString() {
StringBuilder buffy = new StringBuilder("Recorded Events - " + recorderName + '\n');
for (SpyEvent event : events) {
buffy.append(event.toString()).append('\n');
}
Msg.debug(this, buffy.toString());
return buffy.toString();
}
// synchronized because we spy on multiple threads (like Test and Swing)
public void dumpEvents() {
Msg.debug(this, eventsToString());
}
@Override
public String toString() {
return eventsToString();
}
private class SpyEvent {
private FastDateFormat dateFormat = FastDateFormat.getInstance("'T'HH:mm:ssZZ");
private FastDateFormat dateFormat = FastDateFormat.getInstance("'T'HH:mm:ss:SSS");
private int id;
private String message;