From 4e34193bb43db695feecdb82c24c25db886b76e5 Mon Sep 17 00:00:00 2001
From: dragonmacher <48328597+dragonmacher@users.noreply.github.com>
Date: Mon, 22 Mar 2021 17:26:07 -0400
Subject: [PATCH] GP-792 - Updated the TableChooserDialog to allow for clients
to change the sort behavior; deprecated duplicate 'TableChooeserDialog' and
created 'TableSelectionDialog' as a replacement
---
.../app/tablechooser/TableChooserDialog.java | 87 +++++++++---
.../tablechooser/TableChooserDialogTest.java | 133 +++++++++++++++---
.../widgets/dialogs/TableChooserDialog.java | 15 +-
.../widgets/dialogs/TableSelectionDialog.java | 130 +++++++++++++++++
.../table/AbstractSortedTableModel.java | 91 ++++++------
.../widgets/table/ColumnSortState.java | 3 +
6 files changed, 372 insertions(+), 87 deletions(-)
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableSelectionDialog.java
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/tablechooser/TableChooserDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/tablechooser/TableChooserDialog.java
index b9ddf2cacf..46369f05e4 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/tablechooser/TableChooserDialog.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/tablechooser/TableChooserDialog.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
import javax.swing.*;
import javax.swing.table.TableCellRenderer;
@@ -38,7 +39,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.util.HelpLocation;
-import ghidra.util.SystemUtilities;
+import ghidra.util.Swing;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.table.*;
@@ -47,13 +48,13 @@ import ghidra.util.task.TaskMonitor;
import utility.function.Callback;
/**
- * Dialog to show a table of items. If the dialog is constructed with a non-null
+ * Dialog to show a table of items. If the dialog is constructed with a non-null
* {@link TableChooserExecutor}, then a button will be placed in the dialog, allowing the user
* to perform the action defined by the executor.
- *
- *
Each button press will use the selected items as the items to be processed. While the
- * items are scheduled to be processed, they will still be in the table, painted light gray.
- * Attempting to reschedule any of these pending items will have no effect. Each time the
+ *
+ *
Each button press will use the selected items as the items to be processed. While the
+ * items are scheduled to be processed, they will still be in the table, painted light gray.
+ * Attempting to reschedule any of these pending items will have no effect. Each time the
* button is pressed, a new {@link SwingWorker} is created, which will put the processing into
* a background thread. Further, by using multiple workers, the work will be performed in
* parallel.
@@ -113,8 +114,7 @@ public class TableChooserDialog extends DialogComponentProvider
table.installNavigation(goToService, navigatable);
}
table.getSelectionModel()
- .addListSelectionListener(
- e -> setOkEnabled(table.getSelectedRowCount() > 0));
+ .addListSelectionListener(e -> setOkEnabled(table.getSelectedRowCount() > 0));
GhidraTableFilterPanel filterPanel =
new GhidraTableFilterPanel<>(table, model);
@@ -128,12 +128,12 @@ public class TableChooserDialog extends DialogComponentProvider
* @param callback the callback to notify
*/
public void setClosedListener(Callback callback) {
- this.closedCallback = Callback.dummyIfNull(callback);
+ Swing.runNow(() -> closedCallback = Callback.dummyIfNull(callback));
}
/**
* Adds the given object to this dialog. This method can be called from any thread.
- *
+ *
* @param rowObject the object to add
*/
public void add(AddressableRowObject rowObject) {
@@ -141,9 +141,9 @@ public class TableChooserDialog extends DialogComponentProvider
}
/**
- * Removes the given object from this dialog. Nothing will happen if the given item is not
+ * Removes the given object from this dialog. Nothing will happen if the given item is not
* in this dialog. This method can be called from any thread.
- *
+ *
* @param rowObject the object to remove
*/
public void remove(AddressableRowObject rowObject) {
@@ -153,8 +153,7 @@ public class TableChooserDialog extends DialogComponentProvider
private void createTableModel() {
// note: the task monitor is installed later when this model is added to the threaded panel
- SystemUtilities.runSwingNow(
- () -> model = new TableChooserTableModel("Test", tool, program, null));
+ Swing.runNow(() -> model = new TableChooserTableModel("Test", tool, program, null));
}
private void createActions() {
@@ -175,8 +174,8 @@ public class TableChooserDialog extends DialogComponentProvider
};
DockingAction selectionNavigationAction = new SelectionNavigationAction(owner, table);
- selectionNavigationAction.setHelpLocation(
- new HelpLocation(HelpTopics.SEARCH, "Selection_Navigation"));
+ selectionNavigationAction
+ .setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Selection_Navigation"));
addAction(selectAction);
addAction(selectionNavigationAction);
@@ -296,7 +295,49 @@ public class TableChooserDialog extends DialogComponentProvider
}
public void addCustomColumn(ColumnDisplay> columnDisplay) {
- model.addCustomColumn(columnDisplay);
+ Swing.runNow(() -> model.addCustomColumn(columnDisplay));
+ }
+
+ /**
+ * Sets the default sorted column for this dialog.
+ *
+ * This method should be called after all custom columns have been added via
+ * {@link #addCustomColumn(ColumnDisplay)}.
+ *
+ * @param index the view's 0-based column index
+ * @see #setSortState(TableSortState)
+ * @throws IllegalArgumentException if an invalid column is requested for sorting
+ */
+ public void setSortColumn(int index) {
+ setSortState(TableSortState.createDefaultSortState(index));
+ }
+
+ /**
+ * Sets the column sort state for this dialog. The {@link TableSortState} allows for
+ * combinations of sorted columns in ascending or descending order.
+ *
+ *
This method should be called after all custom columns have been added via
+ * {@link #addCustomColumn(ColumnDisplay)}.
+ *
+ * @param state the sort state
+ * @see #setSortColumn(int)
+ * @throws IllegalArgumentException if an invalid column is requested for sorting
+ */
+ public void setSortState(TableSortState state) {
+ AtomicReference ref = new AtomicReference<>();
+ Swing.runNow(() -> {
+ try {
+ model.setTableSortState(state);
+ }
+ catch (IllegalArgumentException e) {
+ ref.set(e);
+ }
+ });
+ IllegalArgumentException exception = ref.get();
+ if (exception != null) {
+ // use a new exception so the stack trace points to this class, not the runnable above
+ throw new IllegalArgumentException(exception);
+ }
}
@Override
@@ -313,15 +354,17 @@ public class TableChooserDialog extends DialogComponentProvider
}
public void clearSelection() {
- table.clearSelection();
+ Swing.runNow(() -> table.clearSelection());
}
public void selectRows(int... rows) {
- ListSelectionModel selectionModel = table.getSelectionModel();
- for (int row : rows) {
- selectionModel.addSelectionInterval(row, row);
- }
+ Swing.runNow(() -> {
+ ListSelectionModel selectionModel = table.getSelectionModel();
+ for (int row : rows) {
+ selectionModel.addSelectionInterval(row, row);
+ }
+ });
}
public int[] getSelectedRows() {
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java
index 304ad880d9..8378c84438 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/tablechooser/TableChooserDialogTest.java
@@ -33,12 +33,15 @@ import docking.action.*;
import docking.actions.KeyEntryDialog;
import docking.actions.ToolActions;
import docking.tool.util.DockingToolConstants;
+import docking.widgets.table.TableSortState;
import ghidra.app.nav.Navigatable;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.DummyPluginTool;
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.Address;
-import ghidra.program.model.address.TestAddress;
-import ghidra.program.model.listing.Program;
+import ghidra.program.model.data.DataType;
+import ghidra.program.model.listing.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.ToyProgramBuilder;
import resources.Icons;
@@ -75,16 +78,48 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
tool = new DummyPluginTool();
tool.setVisible(true);
- Program program = new ToyProgramBuilder("Test", true).getProgram();
+
+ List addresses = new ArrayList<>();
+ ToyProgramBuilder builder = new ToyProgramBuilder("Test", true);
+ builder.createMemory(".text", "0x0", 0x110);
+ Function f = createFunction(builder, 0x00);
+ addresses.add(f.getEntryPoint());
+ f = createFunction(builder, 0x10);
+ addresses.add(f.getEntryPoint());
+ f = createFunction(builder, 0x20);
+ addresses.add(f.getEntryPoint());
+ f = createFunction(builder, 0x30);
+ addresses.add(f.getEntryPoint());
+ f = createFunction(builder, 0x40);
+ addresses.add(f.getEntryPoint());
+ f = createFunction(builder, 0x50);
+ addresses.add(f.getEntryPoint());
+
+ Program program = builder.getProgram();
Navigatable navigatable = null;
dialog = new TableChooserDialog(tool, executor, program, "Dialog Title", navigatable);
testAction = new TestAction();
dialog.addAction(testAction);
+ dialog.addCustomColumn(new OffsetTestColumn());
+ dialog.addCustomColumn(new SpaceTestColumn());
+
dialog.show();
waitForDialogComponent(TableChooserDialog.class);
- loadData();
+ loadData(addresses);
+ }
+
+ private Function createFunction(ProgramBuilder builder, long addr) throws Exception {
+ ProgramDB p = builder.getProgram();
+ FunctionManager fm = p.getFunctionManager();
+ Function f = fm.getFunctionAt(builder.addr(addr));
+ if (f != null) {
+ return f;
+ }
+
+ String a = Long.toHexString(addr);
+ return builder.createEmptyFunction("Function_" + a, "0x" + a, 5, DataType.DEFAULT);
}
private void reCreateDialog(SpyTableChooserExecutor dialogExecutor) throws Exception {
@@ -92,9 +127,9 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
createDialog(dialogExecutor);
}
- private void loadData() {
- for (int i = 0; i < 7; i++) {
- dialog.add(new TestStubRowObject());
+ private void loadData(List addresses) {
+ for (Address a : addresses) {
+ dialog.add(new TestStubRowObject(a));
}
waitForDialog();
@@ -151,7 +186,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
@Test
public void testCalllbackRemovesItems_OtherItemSelected() {
/*
- Select multiple items.
+ Select multiple items.
Have the first callback remove one of the remaining *unselected* items.
The removed item should not itself get a callback.
*/
@@ -187,7 +222,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
public void testCalllbackRemovesItems_OtherItemNotSelected() {
/*
- Select multiple items.
+ Select multiple items.
Have the first callback remove one of the remaining *selected* items.
The removed item should not itself get a callback.
*/
@@ -234,7 +269,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
CountDownLatch continueLatch = new CountDownLatch(1);
testDecision = r -> {
- //
+ //
// Signal that we have started and wait to continue
//
startLatch.countDown();
@@ -308,10 +343,40 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
assertTrue(newToolTip.contains("(A)"));
}
+ @Test
+ public void testSetSortColumn() throws Exception {
+ assertSortedColumn(0);
+ dialog.setSortColumn(1);
+ assertSortedColumn(1);
+ }
+
+ @Test
+ public void testSetSortState() throws Exception {
+ assertSortedColumn(0);
+ dialog.setSortState(TableSortState.createDefaultSortState(2, false));
+ assertSortedColumn(2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetSortState_Invalid() throws Exception {
+ assertSortedColumn(0);
+ dialog.setSortState(TableSortState.createDefaultSortState(100));
+ }
+
//==================================================================================================
// Private Methods
//==================================================================================================
+ private void assertSortedColumn(int expectedColumn) {
+ waitForCondition(() -> expectedColumn == getSortColumn(),
+ "Incorrect sorted column; expected " + expectedColumn + ", found " + getSortColumn());
+ }
+
+ private int getSortColumn() {
+ TableChooserTableModel model = getModel();
+ return runSwing(() -> model.getPrimarySortColumnIndex());
+ }
+
private void setKeyBindingViaF4Dialog(DockingAction action, KeyStroke ks) {
// simulate the user mousing over the toolbar button
@@ -415,6 +480,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
private void waitForDialog() {
waitForCondition(() -> !dialog.isBusy());
+ waitForSwing();
}
private void pressExecuteButton() {
@@ -443,7 +509,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
//==================================================================================================
// Inner Classes
-//==================================================================================================
+//==================================================================================================
private interface TestExecutorDecision {
public boolean decide(AddressableRowObject rowObject);
@@ -485,16 +551,15 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
private static class TestStubRowObject implements AddressableRowObject {
- private static int counter;
- private long addr;
+ private Address addr;
- TestStubRowObject() {
- addr = ++counter;
+ TestStubRowObject(Address a) {
+ this.addr = a;
}
@Override
public Address getAddress() {
- return new TestAddress(addr);
+ return addr;
}
@Override
@@ -503,6 +568,42 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
}
}
+ private static class OffsetTestColumn extends AbstractColumnDisplay {
+
+ @Override
+ public String getColumnValue(AddressableRowObject rowObject) {
+ return Long.toString(rowObject.getAddress().getOffset());
+ }
+
+ @Override
+ public String getColumnName() {
+ return "Offset";
+ }
+
+ @Override
+ public int compare(AddressableRowObject o1, AddressableRowObject o2) {
+ return o1.getAddress().compareTo(o2.getAddress());
+ }
+ }
+
+ private static class SpaceTestColumn extends AbstractColumnDisplay {
+
+ @Override
+ public String getColumnValue(AddressableRowObject rowObject) {
+ return rowObject.getAddress().getAddressSpace().toString();
+ }
+
+ @Override
+ public String getColumnName() {
+ return "Space";
+ }
+
+ @Override
+ public int compare(AddressableRowObject o1, AddressableRowObject o2) {
+ return o1.getAddress().compareTo(o2.getAddress());
+ }
+ }
+
private class TestAction extends DockingAction {
private int invoked;
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableChooserDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableChooserDialog.java
index 4640bfd686..1151c00a79 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableChooserDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableChooserDialog.java
@@ -26,11 +26,14 @@ import docking.DialogComponentProvider;
import docking.widgets.table.*;
/**
- * Dialog for displaying table data in a dialog for the purpose of the user selecting one or
- * more items from the table.
+ * @param the type
*
- * @param The type of row object in the table.
+ *
+ * @deprecated This class has been replaced by {@link TableSelectionDialog}. At the time of
+ * writing, both classes are identical. This version introduced a naming conflict with another
+ * API. Thus, the new version better matches the existing dialog choosing API.
*/
+@Deprecated(forRemoval = true, since = "9.3")
public class TableChooserDialog extends DialogComponentProvider {
private RowObjectTableModel model;
@@ -39,12 +42,14 @@ public class TableChooserDialog extends DialogComponentProvider {
/**
* Create a new Dialog for displaying and choosing table row items
- *
+ *
* @param title The title for the dialog
* @param model a {@link RowObjectTableModel} that has the tRable data
* @param allowMultipleSelection if true, the dialog allows the user to select more
* than one row; otherwise, only single selection is allowed
+ * @deprecated see the class header
*/
+ @Deprecated(forRemoval = true, since = "9.3")
public TableChooserDialog(String title, RowObjectTableModel model,
boolean allowMultipleSelection) {
super(title);
@@ -57,7 +62,9 @@ public class TableChooserDialog extends DialogComponentProvider {
/**
* Returns the list of selected items or null if the dialog was cancelled.
* @return the list of selected items or null if the dialog was cancelled.
+ * @deprecated see the class header
*/
+ @Deprecated(forRemoval = true, since = "9.3")
public List getSelectionItems() {
return selectedItems;
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableSelectionDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableSelectionDialog.java
new file mode 100644
index 0000000000..b0d8f7d2af
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/TableSelectionDialog.java
@@ -0,0 +1,130 @@
+/* ###
+ * 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.dialogs;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.*;
+
+import docking.DialogComponentProvider;
+import docking.widgets.table.*;
+
+/**
+ * Dialog for displaying table data in a dialog for the purpose of the user selecting one or
+ * more items from the table.
+ *
+ * @param The type of row object in the table.
+ */
+public class TableSelectionDialog extends DialogComponentProvider {
+
+ private RowObjectTableModel model;
+ private GFilterTable gFilterTable;
+ private List selectedItems;
+
+ /**
+ * Create a new Dialog for displaying and choosing table row items
+ *
+ * @param title The title for the dialog
+ * @param model a {@link RowObjectTableModel} that has the tRable data
+ * @param allowMultipleSelection if true, the dialog allows the user to select more
+ * than one row; otherwise, only single selection is allowed
+ */
+ public TableSelectionDialog(String title, RowObjectTableModel model,
+ boolean allowMultipleSelection) {
+ super(title);
+ this.model = model;
+ addWorkPanel(buildTable(allowMultipleSelection));
+ addOKButton();
+ addCancelButton();
+ }
+
+ /**
+ * Returns the list of selected items or null if the dialog was cancelled.
+ * @return the list of selected items or null if the dialog was cancelled.
+ */
+ public List getSelectionItems() {
+ return selectedItems;
+ }
+
+ private void initializeTable(boolean allowMultipleSelection) {
+ GTable table = gFilterTable.getTable();
+
+ table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+
+ int selectionMode = allowMultipleSelection ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
+ : ListSelectionModel.SINGLE_SELECTION;
+ table.getSelectionModel().setSelectionMode(selectionMode);
+
+ }
+
+ protected void processMouseClicked(MouseEvent e) {
+
+ if (e.getClickCount() != 2) {
+ return;
+ }
+
+ int rowAtPoint = gFilterTable.getTable().rowAtPoint(e.getPoint());
+ if (rowAtPoint < 0) {
+ return;
+ }
+
+ T selectedRowObject = gFilterTable.getSelectedRowObject();
+ selectedItems = Arrays.asList(selectedRowObject);
+ close();
+ }
+
+ @Override
+ protected void okCallback() {
+ selectedItems = gFilterTable.getSelectedRowObjects();
+ close();
+ gFilterTable.dispose();
+ }
+
+ @Override
+ protected void cancelCallback() {
+ selectedItems = null;
+ close();
+ gFilterTable.dispose();
+ }
+
+ @Override
+ protected void dialogShown() {
+ gFilterTable.focusFilter();
+ }
+
+ private JComponent buildTable(boolean allowMultipleSelection) {
+ gFilterTable = new GFilterTable<>(model);
+ initializeTable(allowMultipleSelection);
+ gFilterTable.getTable().addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (!e.isShiftDown()) {
+ processMouseClicked(e);
+ }
+ updateOkEnabled();
+ }
+ });
+ setOkEnabled(false);
+ return gFilterTable;
+ }
+
+ protected void updateOkEnabled() {
+ setOkEnabled(gFilterTable.getSelectedRowObject() != null);
+ }
+}
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 23e6b68cbb..6981bed897 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
@@ -28,15 +28,15 @@ import ghidra.util.datastruct.WeakSet;
/**
* Table models should extends this model when they want sorting, potentially across multiple
- * columns, but do not want Threading or do not work on Program-related data (Address,
+ * columns, but do not want Threading or do not work on Program-related data (Address,
* ProgramLocations, etc...).
*
- * In order to define custom comparators for a column, simply override
+ * In order to define custom comparators for a column, simply override
* {@link #createSortComparator(int)}. Otherwise, a default comparator will be created for you.
- *
- *
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
+ *
+ *
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 The row type upon which the table is based
@@ -92,11 +92,11 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * Returns the index of the given row object in this model; a negative value if the model
- * does not contain the given object.
- *
- * 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
+ * Returns the index of the given row object in this model; a negative value if the model
+ * does not contain the given object.
+ *
+ *
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
@@ -158,14 +158,15 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
// verify the requested columns are sortable
- for (int i = 0; i < columnCount; i++) {
- ColumnSortState state = tableSortState.getColumnSortState(i);
- if (state == null) {
- continue; // no sort state for this column--nothing to validate
+ for (ColumnSortState state : tableSortState) {
+
+ int index = state.getColumnModelIndex();
+ if (!isSortable(index)) {
+ return false; // the state wants to sort on an unsortable column
}
- if (!isSortable(i)) {
- return false; // the state wants to sort on an unsortable column
+ if (index >= columnCount) {
+ return false; // requested a column that is larger than the number of columns
}
}
@@ -174,10 +175,10 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
/**
* A convenience method that will take a given sort state and remove from it any columns
- * that cannot be sorted. This is useful if the client is restoring a sort state that
- * contains columns that have been removed or are no longer sortable (such as during major
+ * that cannot be sorted. This is useful if the client is restoring a sort state that
+ * contains columns that have been removed or are no longer sortable (such as during major
* table model rewrites).
- *
+ *
* @param state the state
* @return the updated state
*/
@@ -220,9 +221,9 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * Returns true if there is a pending change to the current sort state
+ * Returns true if there is a pending change to the current sort state
* (this includes a sort state that signals no sort will be applied)
- *
+ *
* @return true if there is a pending change to the current sort state
*/
public boolean isSortPending() {
@@ -232,7 +233,7 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
/**
* Returns true if this model has been sorted and does not have a new pending sort that will
* be applied
- *
+ *
* @return true if sorted
* @see #isSortPending()
*/
@@ -245,7 +246,7 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * The default implementation of {@link TableModel#getValueAt(int, int)} that calls the
+ * The default implementation of {@link TableModel#getValueAt(int, int)} that calls the
* abstract {@link #getColumnValueForRow(Object, int)}.
*/
@Override
@@ -255,9 +256,9 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * This method is an attempt to help models that forget to call fireTableDataChanged(). It
- * is expected that tables will fire the notification when they are ready to display data,
- * even if they have that data at construction time. We put this call here so that the
+ * This method is an attempt to help models that forget to call fireTableDataChanged(). It
+ * is expected that tables will fire the notification when they are ready to display data,
+ * even if they have that data at construction time. We put this call here so that the
* forgetful subclasses will have their data sorted for them the first time that this table
* tries to render itself.
*/
@@ -275,11 +276,11 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
/**
* A convenience method for subclasses to quickly/efficiently search for the index of a given
* row object that is visible in the GUI. The visible limitation is due to the
- * fact that the data searched is retrieved from {@link #getModelData()}, which may be
- * filtered.
- *
+ * fact that the data searched is retrieved from {@link #getModelData()}, which may be
+ * filtered.
+ *
* @param rowObject The object for which to search.
- * @return the index of the item in the data returned by
+ * @return the index of the item in the data returned by
*/
@Override
protected int getIndexForRowObject(T rowObject) {
@@ -287,8 +288,8 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * Returns the index for the given object in the given list
- *
+ * Returns the index for the given object in the given list
+ *
* @param rowObject the item
* @param data the data
* @return the index
@@ -312,11 +313,11 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
/**
* A default sort method that uses the {@link Collections#sort(List, Comparator)} method for
* sorting. Implementors with reasonably sized data sets can rely on this method. For data
- * sets that can become large, the ThreadedTableModel
is the recommended base class,
+ * sets that can become large, the ThreadedTableModel
is the recommended base class,
* as it handles loading/sorting/filtering in a threaded way.
- *
+ *
* @param data The data to be sorted
- * @param sortingContext The context required to sort (it contains the sorting columns, a
+ * @param sortingContext The context required to sort (it contains the sorting columns, a
* comparator for sorting, etc...).
*/
protected void sort(List data, TableSortingContext sortingContext) {
@@ -344,7 +345,7 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * Fires an event to let the listeners (like JTable) know that things have been changed.
+ * Fires an event to let the listeners (like JTable) know that things have been changed.
* This method exists so that subclasses have a way to call the various tableChanged()
* methods without triggering this class's overridden version.
* @param dataChanged True signals that the actual data has changed; false signals that the
@@ -366,10 +367,10 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
/**
* An extension point for subclasses to insert their own comparator objects for their data.
- * Subclasses can create comparators for a single or multiple columns, as desired.
- *
+ * Subclasses can create comparators for a single or multiple columns, as desired.
+ *
* @param columnIndex the column index
- * @return the comparator
+ * @return the comparator
*/
protected Comparator createSortComparator(int columnIndex) {
return new RowBasedColumnComparator<>(this, columnIndex, new DefaultColumnComparator(),
@@ -385,7 +386,7 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
// one column sorted and that column is 'reversed', then to provide consistent sorting,
// and later searching, we need to reverse the tie-breaker comparator. Without this,
// the tie-breaker always produces the same results, regardless of which direction the
- // search is going. That will break any clients that call Collections.reverse() in
+ // search is going. That will break any clients that call Collections.reverse() in
// order to quickly invert an existing sort.
//
if (parentChain.primaryComparator instanceof AbstractSortedTableModel.ReverseComparator) {
@@ -419,8 +420,8 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
}
/**
- * Builds a comparator for the given column sort state while allowing for subclasses to
- * provider their own comparators. This method also handles directionality of sorting, so
+ * Builds a comparator for the given column sort state while allowing for subclasses to
+ * provider their own comparators. This method also handles directionality of sorting, so
* that the comparators used can be simple and generic.
*/
private Comparator getComparator(ColumnSortState columnSortState) {
@@ -487,8 +488,8 @@ public abstract class AbstractSortedTableModel extends AbstractGTableModel
public int compare(T t1, T t2) {
// at this point we compare the rows, since all of the sorting column values are equal
- // (Warning: due to comparable being specific to the class upon which it is defined,
- // we have to make sure the class is the same to prevent class cast
+ // (Warning: due to comparable being specific to the class upon which it is defined,
+ // we have to make sure the class is the same to prevent class cast
// exceptions when the table has mixed implementations of 'T')
if (t1 instanceof Comparable && t1.getClass().equals(t2.getClass())) {
return ((Comparable) t1).compareTo(t2);
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ColumnSortState.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ColumnSortState.java
index cda0ca238a..a4962f49d9 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ColumnSortState.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ColumnSortState.java
@@ -55,6 +55,9 @@ public class ColumnSortState {
private int sortOrder_OneBased = -1;
ColumnSortState(int columnModelIndex, SortDirection sortDirection, int sortOrder) {
+ if (columnModelIndex < 0) {
+ throw new IllegalArgumentException("Column index cannot be negative");
+ }
this.columnModelIndex = columnModelIndex;
this.sortDirection = sortDirection;
this.sortOrder_OneBased = sortOrder;