GP-4270: Minor fixes

This commit is contained in:
dragonmacher 2024-01-26 16:18:05 -05:00 committed by Ryan Kurtz
parent b9f914d57c
commit 1cf7803d88
22 changed files with 295 additions and 183 deletions

View file

@ -20,8 +20,10 @@ import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.Objects;
import generic.json.Json;
/**
* A class that holds information used to show a popup menu
* A class that holds information used to show a popup menu
*/
public class PopupMenuContext {
@ -58,4 +60,9 @@ public class PopupMenuContext {
}
return component;
}
@Override
public String toString() {
return Json.toString(this);
}
}

View file

@ -74,10 +74,10 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
@Override
public int getIndexOfFirstMatchingEntry(List<T> list, String text) {
// The data are sorted such that lower-case is before upper-case and smaller length
// matches come before longer matches. If we ever find a case-sensitive exact match,
// use that. Otherwise, keep looking for a case-insensitive exact match. The
// case-insensitive match is preferred over a non-matching item. Once we get to a
// The data are sorted such that lower-case is before upper-case and smaller length
// matches come before longer matches. If we ever find a case-sensitive exact match,
// use that. Otherwise, keep looking for a case-insensitive exact match. The
// case-insensitive match is preferred over a non-matching item. Once we get to a
// non-matching item, we can quit.
int lastPreferredMatchIndex = -1;
for (int i = 0; i < list.size(); i++) {
@ -118,7 +118,7 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
//==================================================================================================
// Inner Classes
//==================================================================================================
//==================================================================================================
private class ObjectStringComparator implements Comparator<Object> {
Comparator<String> stringComparator = new CaseInsensitiveDuplicateStringComparator();

View file

@ -33,7 +33,6 @@ public class ListSelectionDialog<T> extends DialogComponentProvider {
private DropDownSelectionTextField<T> field;
protected boolean cancelled;
private RowObjectTableModel<T> userTableModel;
private DataToStringConverter<T> searchConverter;
private DataToStringConverter<T> descriptionConverter;
private List<T> data;
@ -61,8 +60,12 @@ public class ListSelectionDialog<T> extends DialogComponentProvider {
this.data = data;
this.searchConverter = searchConverter;
this.descriptionConverter = descriptionConverter;
// Use a separate list for the drop down widget, since it needs to sort its data and we do
// not want to change the client data sort.
List<T> dropDownData = new ArrayList<>(data);
DefaultDropDownSelectionDataModel<T> model = new DefaultDropDownSelectionDataModel<>(
new ArrayList<>(data), searchConverter, descriptionConverter) {
dropDownData, searchConverter, descriptionConverter) {
// overridden to return all data for an empty search; this lets the down-arrow
// show the full list
@ -151,14 +154,18 @@ public class ListSelectionDialog<T> extends DialogComponentProvider {
}
private RowObjectTableModel<T> getTableModel() {
if (userTableModel != null) {
return userTableModel;
}
return new DefaultTableModel();
return new DefaultTableModel(data);
}
private class DefaultTableModel extends AbstractGTableModel<T> {
private List<T> modelData;
DefaultTableModel(List<T> modelData) {
// copy the data so that a call to dispose() will not clear the data in the outer class
this.modelData = new ArrayList<>(modelData);
}
@Override
public String getColumnName(int columnIndex) {
if (columnIndex == 0) {
@ -184,7 +191,7 @@ public class ListSelectionDialog<T> extends DialogComponentProvider {
@Override
public List<T> getModelData() {
return data;
return modelData;
}
@Override

View file

@ -29,8 +29,6 @@ import utilities.util.reflection.ReflectionUtilities;
* determines the appropriate cell object for use by the table column this field represents. It can
* then return the appropriate object to display in the table cell for the indicated row object.
*
* Implementations of this interface must provide a public default constructor.
*
* @param <ROW_TYPE> The row object class supported by this column
* @param <COLUMN_TYPE> The column object class supported by this column
* @param <DATA_SOURCE> The object class type that will be passed to see
@ -95,8 +93,8 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
public Class<COLUMN_TYPE> getColumnClass() {
@SuppressWarnings("rawtypes")
Class<? extends AbstractDynamicTableColumn> implementationClass = getClass();
List<Class<?>> typeArguments = ReflectionUtilities.getTypeArguments(
AbstractDynamicTableColumn.class, implementationClass);
List<Class<?>> typeArguments = ReflectionUtilities
.getTypeArguments(AbstractDynamicTableColumn.class, implementationClass);
return (Class<COLUMN_TYPE>) typeArguments.get(1);
}
@ -106,8 +104,8 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
public Class<ROW_TYPE> getSupportedRowType() {
@SuppressWarnings("rawtypes")
Class<? extends AbstractDynamicTableColumn> implementationClass = getClass();
List<Class<?>> typeArguments = ReflectionUtilities.getTypeArguments(
AbstractDynamicTableColumn.class, implementationClass);
List<Class<?>> typeArguments = ReflectionUtilities
.getTypeArguments(AbstractDynamicTableColumn.class, implementationClass);
return (Class<ROW_TYPE>) typeArguments.get(0);
}
@ -193,7 +191,7 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
return getIdentifier().hashCode();
}
// Note: this method is here because the default 'identifier' must be lazy loaded, as
// Note: this method is here because the default 'identifier' must be lazy loaded, as
// at construction time not all the variables needed are available.
private String getIdentifier() {
/*
@ -202,7 +200,7 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
-The case where 2 different column classes share the same column header value
-The case where a single column class is used repeatedly, with a different
column header value each time
Thus, to be unique, we need to combine both the class name and the column header
value. The only time this may be an issue is if the column header value changes
dynamically--not sure if this actually happens anywhere in our system. If it did,

View file

@ -4,9 +4,9 @@
* 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.
@ -20,14 +20,18 @@ import ghidra.framework.plugintool.ServiceProvider;
/**
* This class is meant to be used by DynamicTableColumn implementations that do not care about
* the DATA_SOURCE parameter of DynamicTableColumn. This class will stub the default
* the DATA_SOURCE parameter of DynamicTableColumn. This class will stub the default
* {@link #getValue(Object, Settings, Object, ServiceProvider)} method and
* call a version of the method that does not have the DATA_SOURCE parameter.
* <p>
* Subclasses are not discoverable. To create discoverable columns for the framework, you must
* extends {@link DynamicTableColumnExtensionPoint}.
*
* @param <ROW_TYPE> the row type
* @param <COLUMN_TYPE> the column type
*/
public abstract class AbstractDynamicTableColumnStub<ROW_TYPE, COLUMN_TYPE> extends
AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, Object> {
public abstract class AbstractDynamicTableColumnStub<ROW_TYPE, COLUMN_TYPE>
extends AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, Object> {
@Override
public COLUMN_TYPE getValue(ROW_TYPE rowObject, Settings settings, Object data,

View file

@ -156,7 +156,7 @@ public class AnyObjectTableModel<T> extends GDynamicColumnTableModel<T, Object>
}
private class MethodColumn extends AbstractDynamicTableColumn<T, Object, Object> {
private String name;
private String methodName;
private Method method;
private Class<?> returnType;
@ -166,7 +166,7 @@ public class AnyObjectTableModel<T> extends GDynamicColumnTableModel<T, Object>
init(m);
}
catch (NoSuchMethodException | SecurityException e) {
name = "No method: " + methodName;
this.methodName = "No method: " + methodName;
}
}
@ -181,12 +181,12 @@ public class AnyObjectTableModel<T> extends GDynamicColumnTableModel<T, Object>
private void init(Method m) {
this.method = m;
name = method.getName();
if (name.startsWith("get")) {
name = name.substring(3);
methodName = method.getName();
if (methodName.startsWith("get")) {
methodName = methodName.substring(3);
}
name = fromCamelCase(name);
methodName = fromCamelCase(methodName);
returnType = method.getReturnType();
}
@ -198,15 +198,15 @@ public class AnyObjectTableModel<T> extends GDynamicColumnTableModel<T, Object>
@Override
public String getColumnName() {
return name;
return methodName;
}
@Override
public Object getValue(T rowObject, Settings settings, Object dataSource,
ServiceProvider sp) throws IllegalArgumentException {
if (method == null) {
Msg.error(this,
"No method '" + name + "' on class" + rowObject.getClass().getSimpleName());
Msg.error(this, "No method '" + methodName + "' on class " +
rowObject.getClass().getSimpleName());
return null;
}
try {

View file

@ -76,7 +76,6 @@ public class GFilterTable<ROW_OBJECT> extends JPanel {
}
private void buildThreadedTable() {
@SuppressWarnings("unchecked")
GThreadedTablePanel<ROW_OBJECT> tablePanel =
createThreadedTablePanel((ThreadedTableModel<ROW_OBJECT, ?>) model);
table = tablePanel.getTable();

View file

@ -168,6 +168,20 @@ public class GTableWidget<T> extends JPanel {
return table.getSelectedRowCount();
}
/**
* Sets the selection mode of this table.
*
* @param mode the mode
* @see ListSelectionModel#setSelectionMode(int)
*/
public void setSelectionMode(int mode) {
table.getSelectionModel().setSelectionMode(mode);
}
public int getSelectionMode() {
return table.getSelectionModel().getSelectionMode();
}
public void addSelectionListener(ObjectSelectedListener<T> l) {
gFilterTable.addSelectionListener(l);
}

View file

@ -17,12 +17,13 @@ package docking.widgets.table;
import java.util.*;
import ghidra.util.Msg;
public class TableColumnDescriptor<ROW_TYPE> {
private List<TableColumnInfo> columns = new ArrayList<>();
public List<DynamicTableColumn<ROW_TYPE, ?, ?>> getAllColumns() {
List<DynamicTableColumn<ROW_TYPE, ?, ?>> list =
new ArrayList<>();
List<DynamicTableColumn<ROW_TYPE, ?, ?>> list = new ArrayList<>();
for (TableColumnInfo info : columns) {
list.add(info.column);
}
@ -30,8 +31,7 @@ public class TableColumnDescriptor<ROW_TYPE> {
}
public List<DynamicTableColumn<ROW_TYPE, ?, ?>> getDefaultVisibleColumns() {
List<DynamicTableColumn<ROW_TYPE, ?, ?>> list =
new ArrayList<>();
List<DynamicTableColumn<ROW_TYPE, ?, ?>> list = new ArrayList<>();
for (TableColumnInfo info : columns) {
if (info.isVisible) {
list.add(info.column);
@ -60,20 +60,31 @@ public class TableColumnDescriptor<ROW_TYPE> {
return editor.createTableSortState();
}
private int remove(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
for (int i = 0; i < columns.size(); i++) {
TableColumnDescriptor<ROW_TYPE>.TableColumnInfo info = columns.get(i);
if (info.column == column) {
columns.remove(i);
return i;
}
public void setVisible(String columnName, boolean visible) {
TableColumnInfo info = getColumn(columnName);
if (info == null) {
Msg.debug(this,
"Unable to change visibility state of column '%s'".formatted(columnName));
return;
}
if (visible) {
info.isVisible = true;
}
else {
// remove and add a new info to clear any sort state info for a hidden column
int index = columns.indexOf(info);
columns.set(index, new TableColumnInfo(info.column));
}
return -1;
}
public void setHidden(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
int index = remove(column);
columns.add(index, new TableColumnInfo(column));
private TableColumnInfo getColumn(String name) {
for (TableColumnInfo info : columns) {
String columnName = info.column.getColumnName();
if (columnName.equals(name)) {
return info;
}
}
return null;
}
public void addHiddenColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
@ -105,8 +116,8 @@ public class TableColumnDescriptor<ROW_TYPE> {
this.column = column;
}
TableColumnInfo(DynamicTableColumn<ROW_TYPE, ?, ?> column, boolean isVisible,
int sortIndex, boolean ascending) {
TableColumnInfo(DynamicTableColumn<ROW_TYPE, ?, ?> column, boolean isVisible, int sortIndex,
boolean ascending) {
this.column = column;
this.isVisible = isVisible;
this.sortIndex = sortIndex;

View file

@ -35,9 +35,11 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
/**
* Used to signal that the updateManager has finished loading the final contents gathered
* by this job.
* by this job. By default, the value is 0, which means there is nothing to wait for. If we
* flush, this will be set to 1.
*/
private final CountDownLatch completedCallbackLatch = new CountDownLatch(1);
private CountDownLatch completedCallbackLatch = new CountDownLatch(0);
private volatile boolean isCancelled = false;
private volatile IncrementalUpdatingAccumulator incrementalAccumulator;
@ -74,45 +76,49 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
}
boolean interrupted = Thread.currentThread().isInterrupted();
notifyCompleted(monitor.isCancelled() || interrupted);
notifyCompleted(hasBeenCancelled(monitor) || interrupted);
// all data should have been posted at this point; clean up any data left in the accumulator
incrementalAccumulator.clear();
}
private void doExecute(TaskMonitor monitor) {
try {
threadedModel.doLoad(incrementalAccumulator, monitor);
if (!monitor.isCancelled()) { // in case the model didn't call checkCancelled()
flush(incrementalAccumulator);
}
flush(incrementalAccumulator, monitor);
}
catch (CancelledException e) {
// handled by the caller of this method
isCancelled = true;
}
if (monitor.isCancelled()) {
return; // must leave now or we will block in the call below
}
waitForThreadedTableUpdateManager();
}
private void waitForThreadedTableUpdateManager() {
try {
completedCallbackLatch.await();
}
catch (InterruptedException e) {
// This implies the user has cancelled the job by starting a new one or that we have
// been disposed. Whatever the cause, we want to let the control flow continue as
// normal.
Thread.currentThread().interrupt(); // preserve the interrupt status
}
/**
* This method tracks cancelled from the given monitor and from any cancelled exceptions that
* happen during loading. When loading, the client may trigger a cancelled exception even
* though the monitor has not been cancelled.
* @param monitor the task monitor
* @return true if cancelled
*/
private boolean hasBeenCancelled(TaskMonitor monitor) {
return isCancelled || monitor.isCancelled();
}
private void flush(IncrementalUpdatingAccumulator accumulator) {
private void flush(IncrementalUpdatingAccumulator accumulator, TaskMonitor monitor) {
//
// Acquire the update manager lock so that it doesn't send out any events while we are
// giving it the data we just finished loading.
//
synchronized (updateManager.getSynchronizingLock()) {
if (hasBeenCancelled(monitor)) {
// Check for cancelled inside of this lock. This guarantees that no events will be
// sent out before we can add our listener
return;
}
// push the data to the update manager...
accumulator.flushData();
@ -135,8 +141,23 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
// -A block on jobDone() can now complete as we release the lock
// -jobDone() will notify listeners in an invokeLater(), which puts it behind ours
//
completedCallbackLatch = new CountDownLatch(1);
Swing.runLater(() -> updateManager.addThreadedTableListener(IncrementalLoadJob.this));
}
waitForThreadedTableUpdateManagerToFinish();
}
private void waitForThreadedTableUpdateManagerToFinish() {
try {
completedCallbackLatch.await();
}
catch (InterruptedException e) {
// This implies the user has cancelled the job by starting a new one or that we have
// been disposed. Whatever the cause, we want to let the control flow continue as
// normal.
Thread.currentThread().interrupt(); // preserve the interrupt status
}
}
private void notifyStarted(TaskMonitor monitor) {
@ -151,14 +172,19 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
}
updateManager.removeThreadedTableListener(this);
}
@Override
public void cancel() {
updateManager.getTaskMonitor().cancel();
// monitor.cancel(); TODO: are we handling this ??
super.cancel();
isCancelled = true;
incrementalAccumulator.cancel();
// Note: cannot do this here, since the cancel() call may happen asynchronously and after
// a call to reload() on the table model. Assume that the model itself has already
// cancelled the update manager when the worker queue was cancelled. See
// ThreadedTableModel.reload().
// updateManager.cancelAllJobs();
}
@Override
@ -188,14 +214,15 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
* is being provided to the accumulator.
*/
private class IncrementalUpdatingAccumulator extends SynchronizedListAccumulator<ROW_OBJECT> {
private volatile boolean cancelledOrDone;
private volatile boolean isDone;
private Runnable runnable = () -> {
if (cancelledOrDone) {
if (isCancelledOrDone()) {
// this handles the case where a cancel request came in off the Swing
// thread whilst we were already posted
return;
}
try {
updateManager.reloadSpecificData(asList());
}
@ -203,7 +230,7 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
// note: check for cancelled again, as it may have been called after the initial
// check above if the cancel call was requested off the Swing thread.
if (!cancelledOrDone) {
if (!isCancelledOrDone()) {
Msg.error(this, "Exception incrementally loading table data", e);
}
}
@ -219,8 +246,11 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
swingUpdateManager.update();
}
private boolean isCancelledOrDone() {
return isCancelled || isDone;
}
void cancel() {
cancelledOrDone = true;
swingUpdateManager.dispose();
}
@ -233,7 +263,7 @@ public class IncrementalLoadJob<ROW_OBJECT> extends Job implements ThreadedTable
}
void flushData() {
cancelledOrDone = true;
isDone = true;
swingUpdateManager.dispose();
updateManager.reloadSpecificData(asList());
}

View file

@ -4,9 +4,9 @@
* 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.

View file

@ -145,6 +145,10 @@ public abstract class ThreadedTableModel<ROW_OBJECT, DATA_SOURCE>
updateManager.addThreadedTableListener(new NonIncrementalUpdateManagerListener());
}
startInitialLoad();
}
protected void startInitialLoad() {
// We are expecting to be in the swing thread. We want the reload to happen after our
// constructor is fully completed since the reload will cause our initialize method to
// be called in another thread, thereby creating a possible race condition.

View file

@ -36,27 +36,30 @@ public class TestDataKeyModel extends ThreadedTableModelStub<Long> {
public final static int STRING_COL = 6;
private Byte[] bytes = new Byte[] { Byte.valueOf((byte) 0x09), Byte.valueOf((byte) 0x03),
Byte.valueOf((byte) 0x0c), Byte.valueOf((byte) 0x55), Byte.valueOf((byte) 0x00), Byte.valueOf((byte) 0xdf),
Byte.valueOf((byte) 0xff), Byte.valueOf((byte) 0x03), Byte.valueOf((byte) 0x16), Byte.valueOf((byte) 0x02),
Byte.valueOf((byte) 0x03), Byte.valueOf((byte) 0x04), };
Byte.valueOf((byte) 0x0c), Byte.valueOf((byte) 0x55), Byte.valueOf((byte) 0x00),
Byte.valueOf((byte) 0xdf), Byte.valueOf((byte) 0xff), Byte.valueOf((byte) 0x03),
Byte.valueOf((byte) 0x16), Byte.valueOf((byte) 0x02), Byte.valueOf((byte) 0x03),
Byte.valueOf((byte) 0x04), };
private Short[] shorts = new Short[] { Short.valueOf((short) 0x0841), Short.valueOf((short) 0xb0f7),
Short.valueOf((short) 0xf130), Short.valueOf((short) 0x84e3), Short.valueOf((short) 0x2976),
Short.valueOf((short) 0x17d9), Short.valueOf((short) 0xf146), Short.valueOf((short) 0xc4a5),
Short.valueOf((short) 0x88f1), Short.valueOf((short) 0x966d), Short.valueOf((short) 0x966e),
Short.valueOf((short) 0x966f), };
private Short[] shorts = new Short[] { Short.valueOf((short) 0x0841),
Short.valueOf((short) 0xb0f7), Short.valueOf((short) 0xf130), Short.valueOf((short) 0x84e3),
Short.valueOf((short) 0x2976), Short.valueOf((short) 0x17d9), Short.valueOf((short) 0xf146),
Short.valueOf((short) 0xc4a5), Short.valueOf((short) 0x88f1), Short.valueOf((short) 0x966d),
Short.valueOf((short) 0x966e), Short.valueOf((short) 0x966f), };
private Integer[] ints =
new Integer[] { Integer.valueOf(0x039D492B), Integer.valueOf(0x0A161497), Integer.valueOf(0x06AA1497),
Integer.valueOf(0x0229EE9E), Integer.valueOf(0xFB7428E1), Integer.valueOf(0xD2B4ED2F),
Integer.valueOf(0x0C1F67DE), Integer.valueOf(0x0E61C987), Integer.valueOf(0x0133751F),
Integer.valueOf(0x07B39541), Integer.valueOf(0x07B39542), Integer.valueOf(0x07B39542), };
private Integer[] ints = new Integer[] { Integer.valueOf(0x039D492B),
Integer.valueOf(0x0A161497), Integer.valueOf(0x06AA1497), Integer.valueOf(0x0229EE9E),
Integer.valueOf(0xFB7428E1), Integer.valueOf(0xD2B4ED2F), Integer.valueOf(0x0C1F67DE),
Integer.valueOf(0x0E61C987), Integer.valueOf(0x0133751F), Integer.valueOf(0x07B39541),
Integer.valueOf(0x07B39542), Integer.valueOf(0x07B39542), };
private Long[] longs = new Long[] { Long.valueOf(0x0000000DFAA00C4FL),
Long.valueOf(0x00000001FD7CA6A6L), Long.valueOf(0xFFFFFFF4D0EB4AB8L), Long.valueOf(0x0000000445246143L),
Long.valueOf(0xFFFFFFF5696F1780L), Long.valueOf(0x0000000685526E5DL), Long.valueOf(0x00000009A1FD98EEL),
Long.valueOf(0x00000004AD2B1869L), Long.valueOf(0x00000002928E64C8L), Long.valueOf(0x000000071CE1DDB2L),
Long.valueOf(0x000000071CE1DDB3L), Long.valueOf(0x000000071CE1DDB4L), };
private Long[] longs =
new Long[] { Long.valueOf(0x0000000DFAA00C4FL), Long.valueOf(0x00000001FD7CA6A6L),
Long.valueOf(0xFFFFFFF4D0EB4AB8L), Long.valueOf(0x0000000445246143L),
Long.valueOf(0xFFFFFFF5696F1780L), Long.valueOf(0x0000000685526E5DL),
Long.valueOf(0x00000009A1FD98EEL), Long.valueOf(0x00000004AD2B1869L),
Long.valueOf(0x00000002928E64C8L), Long.valueOf(0x000000071CE1DDB2L),
Long.valueOf(0x000000071CE1DDB3L), Long.valueOf(0x000000071CE1DDB4L), };
private Float[] floats =
new Float[] { Float.valueOf((float) 0.143111240), Float.valueOf((float) 0.084097680),
@ -77,7 +80,7 @@ public class TestDataKeyModel extends ThreadedTableModelStub<Long> {
protected String[] strings = new String[] { "one", "two", "THREE", "Four", "FiVe", "sIx",
"SeVEn", "EighT", "NINE", "ten", "ten", "ten" };
private long timeBetweenAddingDataItemsInMillis = 1;
private volatile long timeBetweenAddingDataItemsInMillis = 1;
private volatile IncrementalLoadJob<Long> loadJob = null;