GT-3396 - File Chooser - Implement quick lookup by typing in the table

or list views
This commit is contained in:
dragonmacher 2019-12-12 16:09:57 -05:00
parent 3eb130123b
commit 171914f49e
9 changed files with 652 additions and 206 deletions

View File

@ -31,11 +31,13 @@ public class GTableTest extends AbstractGhidraHeadedIntegrationTest {
private TestDataModel model;
private GhidraTable table;
private JFrame frame;
private long testKeyTimeout = 100;
@Before
public void setUp() throws Exception {
model = new TestDataModel();
table = new GhidraTable(model);
table.setAutoLookupTimeout(testKeyTimeout);
frame = new JFrame("Ghidra Table Test");
frame.getContentPane().setLayout(new BorderLayout());
@ -72,6 +74,84 @@ public class GTableTest extends AbstractGhidraHeadedIntegrationTest {
timeout();
triggerText(table, "a");
assertEquals(11, table.getSelectedRow());
// test the case where no match is found
table.setAutoLookupTimeout(1000); // longer timeout needed for multiple keys
triggerText(table, "zed");
assertEquals(11, table.getSelectedRow()); // no change
}
@Test
public void testAutoLookup_SortDescending() throws Exception {
int column = 4;
sortDescending(column);
table.setAutoLookupColumn(column);
setSelectedRow(table, 0);
triggerText(table, "a");
assertEquals(1846, table.getSelectedRow());
triggerText(table, "c");
assertEquals(1902, table.getSelectedRow());
timeout();
triggerText(table, "ad");
assertEquals(1885, table.getSelectedRow());
timeout();
triggerText(table, "av");
assertEquals(1848, table.getSelectedRow());
timeout();
triggerText(table, "x");
assertEquals(0, table.getSelectedRow());
timeout();
triggerText(table, "a");
assertEquals(1846, table.getSelectedRow());
// test the case where no match is found
table.setAutoLookupTimeout(1000); // longer timeout needed for multiple keys
triggerText(table, "zed");
assertEquals(1846, table.getSelectedRow()); // no change
}
@Test
public void testAutoLookup_WhenColumnIsNotSorted() throws Exception {
int column = 4;
removeSortColumn(column);
table.setAutoLookupColumn(column);
setSelectedRow(table, 0);
// note: the order checked here is the same as the sorted order, since we did not move
// any rows after disabling the sort
triggerText(table, "a");
assertEquals(11, table.getSelectedRow());
triggerText(table, "c");
assertEquals(12, table.getSelectedRow());
timeout();
triggerText(table, "ad");
assertEquals(24, table.getSelectedRow());
timeout();
triggerText(table, "av");
assertEquals(70, table.getSelectedRow());
timeout();
triggerText(table, "x");
assertEquals(1920, table.getSelectedRow());
timeout();
triggerText(table, "a");
assertEquals(11, table.getSelectedRow());
// test the case where no match is found
table.setAutoLookupTimeout(1000); // longer timeout needed for multiple keys
triggerText(table, "zed");
assertEquals(11, table.getSelectedRow()); // no change
}
@Test
@ -112,8 +192,21 @@ public class GTableTest extends AbstractGhidraHeadedIntegrationTest {
assertEquals("Auto-lookup failed to change the table row", 11, table.getSelectedRow());
}
private void timeout() throws InterruptedException {
Thread.sleep(GTable.KEY_TIMEOUT * 2);
private void removeSortColumn(int column) {
waitForSwing();
runSwing(() -> TableUtils.columnAlternativelySelected(table, column));
waitForSwing();
}
private void sortDescending(int column) {
TableSortState descendingSortState = TableSortState.createDefaultSortState(column, false);
runSwing(() -> model.setTableSortState(descendingSortState));
waitForSwing();
}
private void timeout() {
sleep(testKeyTimeout * 2);
}
private void setSelectedRow(final GhidraTable table, final int i) throws Exception {

View File

@ -392,24 +392,26 @@ public class SymbolTablePluginTest extends AbstractGhidraHeadedIntegrationTest {
});
waitForNotBusy(symbolTable);
int testTimeoutMs = 100;
symbolTable.setAutoLookupTimeout(testTimeoutMs);
selectRow(0);
triggerAutoLookup("a");
assertEquals(findRow("a", "Global"), symbolTable.getSelectedRow());
sleep(GTable.KEY_TIMEOUT);
sleep(testTimeoutMs);
triggerAutoLookup("ab");
assertEquals(findRow("ab", "Global"), symbolTable.getSelectedRow());
sleep(GTable.KEY_TIMEOUT);
sleep(testTimeoutMs);
triggerAutoLookup("abc");
assertEquals(findRow("abc", "Global"), symbolTable.getSelectedRow());
sleep(GTable.KEY_TIMEOUT);
sleep(testTimeoutMs);
triggerAutoLookup("abcd");
assertEquals(findRow("abc1", "Global"), symbolTable.getSelectedRow());
sleep(GTable.KEY_TIMEOUT);
sleep(testTimeoutMs);
selectRow(0);
triggerAutoLookup("abc12");

View File

@ -18,10 +18,12 @@
*/
package docking.widgets.filechooser;
import static org.apache.commons.lang3.StringUtils.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.ArrayList;
import java.util.*;
import java.util.List;
import javax.swing.*;
@ -45,6 +47,10 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
private JTextField listEditorField;
private JPanel listEditor;
private long keyTimeout = AUTO_LOOKUP_TIMEOUT;
private long lastLookupTime;
private String lastLookupText;
/** The file being edited */
private File editedFile;
@ -110,32 +116,23 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() != KeyEvent.VK_ENTER) {
return;
}
e.consume();
int[] selectedIndices = getSelectedIndices();
if (selectedIndices.length == 0) {
chooser.okCallback();
// this implies the user has somehow put focus into the table, but has not
// made a selection...just let the chooser decide what to do
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
e.consume();
handleEnterKey();
return;
}
if (selectedIndices.length > 1) {
// let the chooser decide what to do with multiple rows selected
chooser.okCallback();
return;
}
File file = model.getFile(selectedIndices[0]);
if (chooser.getModel().isDirectory(file)) {
chooser.setCurrentDirectory(file);
String eventChar = Character.toString(e.getKeyChar());
long when = e.getWhen();
if (when - lastLookupTime > keyTimeout) {
lastLookupText = eventChar;
}
else {
chooser.userChoseFile(file);
lastLookupText += eventChar;
}
lastLookupTime = when;
lookupText(lastLookupText);
}
});
@ -213,6 +210,100 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
add(listEditor);
}
private void lookupText(String text) {
if (text == null) {
return;
}
int row = getSelectedIndex();
int rows = getModel().getSize();
if (row >= 0 && row < rows - 1) {
if (text.length() == 1) {
// fresh search; ignore the current row, could be from a previous match
++row;
}
File file = getModel().getElementAt(row);
String name = chooser.getDisplayName(file);
if (!name.isEmpty() && startsWithIgnoreCase(name, text)) {
setSelectedFile(getFile(row));
return;
}
}
int index = autoLookupBinary(text);
if (index >= 0) {
setSelectedFile(getFile(index));
}
}
private int autoLookupBinary(String text) {
// caveat: for this search to work, the data must be ascending sorted
List<File> files = model.getAllFiles();
File key = new File(lastLookupText);
Comparator<File> comparator = (f1, f2) -> {
String n1 = chooser.getDisplayName(f1);
return compareIgnoreCase(n1, text);
};
int index = Collections.binarySearch(files, key, comparator);
if (index < 0) {
index = -index - 1;
}
File file = files.get(index);
String name = chooser.getDisplayName(file);
if (startsWithIgnoreCase(name, text)) {
return index;
}
int before = index - 1;
if (before >= 0) {
file = files.get(before);
name = chooser.getDisplayName(file);
if (startsWithIgnoreCase(name, text)) {
return before;
}
}
int after = index + 1;
if (after < files.size()) {
file = files.get(after);
name = chooser.getDisplayName(file);
if (startsWithIgnoreCase(name, text)) {
return after;
}
}
return -1;
}
private void handleEnterKey() {
int[] selectedIndices = getSelectedIndices();
if (selectedIndices.length == 0) {
chooser.okCallback();
// this implies the user has somehow put focus into the table, but has not
// made a selection...just let the chooser decide what to do
return;
}
if (selectedIndices.length > 1) {
// let the chooser decide what to do with multiple rows selected
chooser.okCallback();
return;
}
File file = model.getFile(selectedIndices[0]);
if (chooser.getModel().isDirectory(file)) {
chooser.setCurrentDirectory(file);
}
else {
chooser.userChoseFile(file);
}
}
private void maybeSelectItem(MouseEvent e) {
Point point = e.getPoint();
int index = locationToIndex(point);
@ -297,6 +388,17 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
}
}
/**
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
* @param timeout the timeout
* @see #AUTO_LOOKUP_TIMEOUT
*/
public void setAutoLookupTimeout(long timeout) {
keyTimeout = timeout;
lastLookupText = null;
lastLookupTime = 0;
}
void setSelectedFiles(Iterable<File> files) {
List<Integer> indexes = new ArrayList<>();

View File

@ -815,22 +815,21 @@ public class GhidraFileChooser extends DialogComponentProvider
}
String getDisplayName(File file) {
if (file != null) {
if (GhidraFileChooser.MY_COMPUTER.equals(getCurrentDirectory())) {
String str = getModel().getDescription(file);
if (str == null || str.length() == 0) {
str = file.getAbsolutePath();
}
return str;
}
else if (GhidraFileChooser.RECENT.equals(getCurrentDirectory())) {
return file.getAbsolutePath() + " ";
}
else {
return getFilename(file) + " ";
}
if (file == null) {
return "";
}
return "";
if (GhidraFileChooser.MY_COMPUTER.equals(getCurrentDirectory())) {
String str = getModel().getDescription(file);
if (str == null || str.length() == 0) {
str = file.getAbsolutePath();
}
return str;
}
else if (GhidraFileChooser.RECENT.equals(getCurrentDirectory())) {
return file.getAbsolutePath() + " ";
}
return getFilename(file) + " ";
}
private void setDirectoryList(File directory, List<File> files) {

View File

@ -36,7 +36,7 @@ import docking.widgets.table.GTable;
public class GList<T> extends JList<T> implements GComponent {
/**The timeout for the auto-lookup feature*/
public static final long KEY_TIMEOUT = GTable.KEY_TIMEOUT;//made public for JUnits...
public static final long AUTO_LOOKUP_TIMEOUT = GTable.AUTO_LOOKUP_TIMEOUT;
/**
* Constructs a <code>GhidraList</code> with an empty model.

View File

@ -29,6 +29,8 @@ import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import org.apache.commons.lang3.StringUtils;
import docking.*;
import docking.action.*;
import docking.actions.KeyBindingUtils;
@ -80,14 +82,16 @@ public class GTable extends JTable {
KeyStroke.getKeyStroke(KeyEvent.VK_A, CONTROL_KEY_MODIFIER_MASK);
private static final String LAST_EXPORT_FILE = "LAST_EXPORT_DIR";
private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke("ESCAPE");
private int userDefinedRowHeight;
public static final long AUTO_LOOKUP_TIMEOUT = 800;
private static final int AUTO_LOOKUP_MAX_SEARCH_ROWS = 50000;
private long keyTimeout = AUTO_LOOKUP_TIMEOUT;
private boolean isInitialized;
private boolean enableActionKeyBindings;
private KeyListener autoLookupListener;
private long lastLookupTime;
private String lookupString;
private AutoLookupResult lastLookup;
private int lookupColumn = -1;
/** A list of default renderers created by this table */
@ -106,10 +110,9 @@ public class GTable extends JTable {
private SelectionManager selectionManager;
private Integer visibleRowCount;
public static final long KEY_TIMEOUT = 800;//made public for JUnits...
private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke("ESCAPE");
private int userDefinedRowHeight;
private TableModelListener rowHeightListener = e -> adjustRowHeight();
private TableColumnModelListener tableColumnModelListener = null;
private final Map<Integer, GTableCellRenderingData> columnRenderingDataMap = new HashMap<>();
@ -273,115 +276,144 @@ public class GTable extends JTable {
}
}
private int getRow(TableModel model, String keyString) {
if (keyString == null) {
private String getValueString(int row, int col) {
TableCellRenderer renderer = getCellRenderer(row, col);
if (renderer instanceof JLabel) {
prepareRenderer(renderer, row, col);
return ((JLabel) renderer).getText();
}
Object obj = getValueAt(row, col);
return obj == null ? null : obj.toString();
}
private int lookupText(String text) {
if (text == null) {
return -1;
}
int currRow = getSelectedRow();
if (currRow >= 0 && currRow < getRowCount() - 1) {
if (keyString.length() == 1) {
++currRow;
int row = getSelectedRow();
if (row >= 0 && row < getRowCount() - 1) {
if (text.length() == 1) {
// fresh search; ignore the current row, could be from a previous match
++row;
}
Object obj = getValueAt(currRow, convertColumnIndexToView(lookupColumn));
if (obj != null && obj.toString().toLowerCase().startsWith(keyString.toLowerCase())) {
return currRow;
int col = convertColumnIndexToView(lookupColumn);
if (textMatches(text, row, col)) {
return row;
}
}
if (model instanceof SortedTableModel) {
SortedTableModel sortedModel = (SortedTableModel) model;
if (dataModel instanceof SortedTableModel) {
SortedTableModel sortedModel = (SortedTableModel) dataModel;
if (lookupColumn == sortedModel.getPrimarySortColumnIndex()) {
return autoLookupBinary(sortedModel, keyString);
return autoLookupBinary(sortedModel, text);
}
}
return autoLookupLinear(keyString);
return autoLookupLinear(text);
}
private int autoLookupLinear(String keyString) {
int rowCount = getRowCount();
int startRow = getSelectedRow();
private boolean textMatches(String text, int row, int col) {
String value = getValueString(row, col);
return StringUtils.startsWithIgnoreCase(value, text);
}
private int autoLookupLinear(String text) {
int max = AUTO_LOOKUP_MAX_SEARCH_ROWS;
int rows = getRowCount();
int start = getSelectedRow();
int counter = 0;
int col = convertColumnIndexToView(lookupColumn);
for (int i = startRow + 1; i < rowCount; i++) {
Object obj = getValueAt(i, col);
if (obj != null && obj.toString().toLowerCase().startsWith(keyString.toLowerCase())) {
// first search from the current row until the last row
for (int i = start + 1; i < rows && counter < max; i++, counter++) {
if (textMatches(text, i, col)) {
return i;
}
if (counter++ > TableUtils.MAX_SEARCH_ROWS) {
return -1;
}
}
for (int i = 0; i < startRow; i++) {
Object obj = getValueAt(i, col);
if (obj != null && obj.toString().toLowerCase().startsWith(keyString.toLowerCase())) {
// then wrap the search to be from the beginning to the current row
for (int i = 0; i < start && counter < max; i++, counter++) {
if (textMatches(text, i, col)) {
return i;
}
if (counter++ > TableUtils.MAX_SEARCH_ROWS) {
return -1;
}
}
return -1;
}
private int autoLookupBinary(SortedTableModel model, String keyString) {
String modifiedLookupString = keyString;
private int autoLookupBinary(SortedTableModel model, String text) {
int index = binarySearch(model, text);
int col = convertColumnIndexToView(lookupColumn);
if (textMatches(text, index, col)) {
return index;
}
if (index - 1 >= 0) {
if (textMatches(text, index - 1, col)) {
return index - 1;
}
}
if (index + 1 < model.getRowCount()) {
if (textMatches(text, index + 1, col)) {
return index + 1;
}
}
return -1;
}
private int binarySearch(SortedTableModel model, String text) {
int sortedOrder = 1;
int primarySortColumnIndex = model.getPrimarySortColumnIndex();
TableSortState columnSortState = model.getTableSortState();
ColumnSortState sortState = columnSortState.getColumnSortState(primarySortColumnIndex);
// if sorted descending, then reverse the search direction and change the lookup text to
// so that a match will come after the range we seek, which is before the desired text
// when sorted in reverse
if (!sortState.isAscending()) {
sortedOrder = -1;
int lastCharPos = modifiedLookupString.length() - 1;
char lastChar = modifiedLookupString.charAt(lastCharPos);
int lastPos = text.length() - 1;
char lastChar = text.charAt(lastPos);
++lastChar;
modifiedLookupString = modifiedLookupString.substring(0, lastCharPos) + lastChar;
text = text.substring(0, lastPos) + lastChar;
}
int min = 0;
int max = model.getRowCount() - 1;
int rows = model.getRowCount();
int max = rows - 1;
int col = convertColumnIndexToView(lookupColumn);
while (min < max) {
int i = (min + max) / 2;
Object obj = getValueAt(i, col);
if (obj == null) {
obj = "";
}
int compare = modifiedLookupString.toString().compareToIgnoreCase(obj.toString());
int mid = (min + max) / 2;
String value = getValueString(mid, col);
int compare = text.compareToIgnoreCase(value);
compare *= sortedOrder;
if (compare < 0) {
max = i - 1;
max = mid - 1;
}
else if (compare > 0) {
min = i + 1;
min = mid + 1;
}
else {//compare == 0, MATCH!
return i;
else { // exact match
return mid;
}
}
String value = getValueAt(min, col).toString();
if (value.toLowerCase().startsWith(keyString.toLowerCase())) {
return min;
}
if (min - 1 >= 0) {
value = getValueAt(min - 1, col).toString();
if (value.toLowerCase().startsWith(keyString.toLowerCase())) {
return min - 1;
}
}
if (min + 1 < dataModel.getRowCount()) {
value = getValueAt(min + 1, col).toString();
if (value.toLowerCase().startsWith(keyString.toLowerCase())) {
return min + 1;
}
}
return min;
}
return -1;
/**
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
* @param timeout the timeout
* @see #setAutoLookupColumn(int)
* @see #AUTO_LOOKUP_TIMEOUT
*/
public void setAutoLookupTimeout(long timeout) {
keyTimeout = timeout;
lastLookup = null;
}
/**
@ -409,25 +441,25 @@ public class GTable extends JTable {
return;
}
if (isIgnorableKeyEvent(e)) {
AutoLookupResult lookup = lastLookup;
if (lookup == null) {
lookup = new AutoLookupResult();
}
lookup.keyTyped(e);
if (lookup.shouldSkip()) {
return;
}
long when = e.getWhen();
if (when - lastLookupTime > KEY_TIMEOUT) {
lookupString = "" + e.getKeyChar();
}
else {
lookupString += "" + e.getKeyChar();
}
int row = getRow(dataModel, lookupString);
int row = lookupText(lookup.getText());
lookup.setFoundMatch(row >= 0);
if (row >= 0) {
setRowSelectionInterval(row, row);
Rectangle rect = getCellRect(row, 0, false);
scrollRectToVisible(rect);
}
lastLookupTime = when;
lastLookup = lookup;
}
};
}
@ -698,9 +730,6 @@ public class GTable extends JTable {
return super.processKeyBinding(ks, e, condition, pressed);
}
/**
* @see javax.swing.JTable#getDefaultRenderer(java.lang.Class)
*/
@Override
public TableCellRenderer getDefaultRenderer(Class<?> columnClass) {
if (columnClass == null) {
@ -1546,4 +1575,50 @@ public class GTable extends JTable {
return sourceComponent instanceof GTable;
}
}
private class AutoLookupResult {
private long lastTime;
private String text;
private boolean foundPreviousMatch;
private boolean skip;
public void keyTyped(KeyEvent e) {
skip = false;
if (isIgnorableKeyEvent(e)) {
skip = true;
return;
}
String eventChar = Character.toString(e.getKeyChar());
long when = e.getWhen();
if (when - lastTime > keyTimeout) {
text = eventChar;
}
else {
text += eventChar;
if (!foundPreviousMatch) {
// The given character is being added to the previous search. If that search
// was fruitless, then so too will be this one, since we use a
// 'starts with' match.
skip = true;
}
}
lastTime = when;
}
void setFoundMatch(boolean foundMatch) {
foundPreviousMatch = foundMatch;
}
String getText() {
return text;
}
boolean shouldSkip() {
return skip;
}
}
}

View File

@ -23,8 +23,6 @@ import javax.swing.table.*;
*/
public class TableUtils {
public static final int MAX_SEARCH_ROWS = 50000;
/**
* Attempts to sort the given table based upon the given column index. If the {@link TableModel}
* of the given table is not a {@link SortedTableModel}, then this method will do nothing.

View File

@ -22,13 +22,15 @@ import static docking.widgets.filechooser.GhidraFileChooserMode.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.*;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@ -48,8 +50,7 @@ import docking.action.DockingAction;
import docking.test.AbstractDockingTest;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.SpyDropDownWindowVisibilityListener;
import docking.widgets.table.ColumnSortState;
import docking.widgets.table.TableSortState;
import docking.widgets.table.*;
import generic.concurrent.ConcurrentQ;
import ghidra.framework.*;
import ghidra.framework.preferences.Preferences;
@ -151,7 +152,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
waitForChooser();
File newCurrentDirectory = getCurrentDirectory();
assertTrue(!currentDirectory.equals(newCurrentDirectory));
assertFalse(currentDirectory.equals(newCurrentDirectory));
JButton backButton = (JButton) getInstanceField("backButton", chooser);
assertTrue(backButton.isEnabled());
@ -180,7 +181,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
waitForChooser();
waitForSwing();
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
final File newFile = getNewlyCreatedFile(dirlist);
waitForFile(newFile, DEFAULT_TIMEOUT_MILLIS);
stopListEdit(dirlist);
@ -244,7 +245,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
// hack: the focus listeners can trigger an editCancelled(), which is a problem in
// parallel mode
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
removeFocusListeners(dirlist);
pressNewFolderButton();
@ -276,7 +277,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
public void testNewFolderInList() throws Exception {
setMode(DIRECTORIES_ONLY);
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
// hack: the focus listeners can trigger an editCancelled(), which is a problem in
// parallel mode
@ -407,10 +408,10 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
File tempfile = File.createTempFile(getName(), ".tmp", tempdir);
tempfile.deleteOnExit();
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
assertNotNull(dirlist);
DirectoryListModel dirmodel = (DirectoryListModel) dirlist.getModel();
assertTrue(!dirmodel.contains(tempfile));
assertFalse(dirmodel.contains(tempfile));
pressRefreshButton();
waitForChooser();
@ -420,7 +421,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
pressRefreshButton();
waitForChooser();
assertTrue(!dirmodel.contains(tempfile));
assertFalse(dirmodel.contains(tempfile));
// verify back stack is not corrupted!!!
assertEquals(size, history.size());
@ -434,7 +435,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
pressNewFolderButton();
waitForSwing();
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
File newFile = getNewlyCreatedFile(dirlist);
waitForFile(newFile, DEFAULT_TIMEOUT_MILLIS);
@ -450,7 +451,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
waitForFile(newFile, DEFAULT_TIMEOUT_MILLIS);
// verify we did not go into the selected directory
assertTrue(!newFile.equals(getCurrentDirectory()));
assertFalse(newFile.equals(getCurrentDirectory()));
}
/*
@ -700,7 +701,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
@Test
public void testDirectoryInDirectory() throws Exception {
setMode(FILES_AND_DIRECTORIES);
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
// hack: the focus listeners can trigger an editCancelled(), which is a problem in
// parallel mode
@ -961,24 +962,24 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
public void testShowDetails() throws Exception {
JPanel cardPanel = (JPanel) findComponentByName(chooser.getComponent(), "CARD_PANEL");
JList<?> dirlist = (JList<?>) findComponentByName(chooser.getComponent(), "LIST");
JTable dirtable = (JTable) findComponentByName(chooser.getComponent(), "TABLE");
JTable dirtable = getTableView();
JScrollPane scrollpane1 = (JScrollPane) cardPanel.getComponent(0);
JScrollPane scrollpane2 = (JScrollPane) cardPanel.getComponent(1);
assertEquals(dirtable, scrollpane1.getViewport().getComponent(0));
assertEquals(dirlist, scrollpane2.getViewport().getComponent(0));
assertTrue(!scrollpane1.isVisible());
assertFalse(scrollpane1.isVisible());
assertTrue(scrollpane2.isVisible());
pressDetailsButton();
waitForChooser();
assertTrue(scrollpane1.isVisible());
assertTrue(!scrollpane2.isVisible());
assertFalse(scrollpane2.isVisible());
pressDetailsButton();
waitForChooser();
assertTrue(!scrollpane1.isVisible());
assertFalse(scrollpane1.isVisible());
assertTrue(scrollpane2.isVisible());
close();
}
@ -987,7 +988,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
public void testSortingByFileName() throws Exception {
pressDetailsButton();
waitForSwing();
JTable dirtable = (JTable) findComponentByName(chooser.getComponent(), "TABLE");
JTable dirtable = getTableView();
DirectoryTableModel model = (DirectoryTableModel) dirtable.getModel();
JTableHeader header = dirtable.getTableHeader();
Rectangle rect = header.getHeaderRect(DirectoryTableModel.FILE_COL);
@ -1004,7 +1005,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
@Test
public void testSortingByFileSzie() throws Exception {
pressDetailsButton();
JTable dirtable = (JTable) findComponentByName(chooser.getComponent(), "TABLE");
JTable dirtable = getTableView();
DirectoryTableModel model = (DirectoryTableModel) dirtable.getModel();
JTableHeader header = dirtable.getTableHeader();
Rectangle rect = header.getHeaderRect(DirectoryTableModel.SIZE_COL);
@ -1021,7 +1022,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
@Test
public void testSortingByFileDate() throws Exception {
pressDetailsButton();
JTable dirtable = (JTable) findComponentByName(chooser.getComponent(), "TABLE");
JTable dirtable = getTableView();
DirectoryTableModel model = (DirectoryTableModel) dirtable.getModel();
JTableHeader header = dirtable.getTableHeader();
Rectangle rect = header.getHeaderRect(DirectoryTableModel.TIME_COL);
@ -1103,7 +1104,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
setFile(regularFile);
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
File file = getSelectedFile(dirlist, DEFAULT_TIMEOUT_MILLIS);
assertNotNull(file);
assertEquals(regularFile.getName().toUpperCase(), file.getName().toUpperCase());
@ -1153,7 +1154,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
File directory = new File(getTestDirectoryPath());
setFile(directory);
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
File selectedFile = getSelectedFile(dirlist, DEFAULT_TIMEOUT_MILLIS);
assertNotNull(selectedFile);
assertEquals(directory.getName().toUpperCase(), selectedFile.getName().toUpperCase());
@ -1325,23 +1326,6 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
assertEquals(wantedFile, selectedFile);
}
private List<File> getExistingFiles(File dir, int count) {
assertTrue(dir.isDirectory());
File[] files = dir.listFiles(f -> f.isFile());
assertTrue("Dir does not contain enough files - '" + dir + "'; count = " + count,
files.length >= count);
// create some consistency between runs
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
List<File> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(files[i]);
}
return result;
}
@Test
public void testRenameInList() throws Exception {
doRenameTest(chooser.getActionManager(), "LIST_EDITOR_FIELD");
@ -1349,7 +1333,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
@Test
public void testRenameInTable() throws Exception {
pressDetailsButton();
setTableMode();
waitForSwing();
doRenameTest(chooser.getActionManager(), "TABLE_EDITOR_FIELD");
}
@ -1545,31 +1529,6 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
assertFalse(containsRecentFile(file));
}
private boolean containsRecentFile(File file) {
@SuppressWarnings("unchecked")
List<RecentGhidraFile> recents =
(List<RecentGhidraFile>) getInstanceField("recentList", chooser);
for (RecentGhidraFile recent : recents) {
File actual = recent.getAbsoluteFile();
if (file.equals(actual)) {
return true;
}
}
return false;
}
private ActionContext createDirListContext() {
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
return new ActionContext(null, dirlist);
}
private boolean isEnabled(DockingAction action, ActionContext context) {
return runSwing(() -> action.isEnabledForContext(context));
}
@Test
public void testFileFilter() throws Exception {
JList<?> dirlist = (JList<?>) findComponentByName(chooser.getComponent(), "LIST");
@ -1671,6 +1630,109 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
assertEquals(!wasSelected, isSelected);
}
@Test
public void testFilenameAutoLookup_InTable() throws Exception {
// Note: the table auto lookup is tested elsewhere. This test is just making sure that
// the feature responds within the file chooser.
// dir file names start with 'a_...', 'b_...', etc
TestFiles files = createAlphabeticMixedDirectory();
showMultiSelectionChooser(files.parent, FILES_ONLY);
setTableMode();
GTable table = getTableView();
int testTimeoutMs = 100;
table.setAutoLookupTimeout(testTimeoutMs);
selectFile(table, 0);
focus(table);
triggerText(table, "b");
assertSelectedIndex(table, 1);
sleep(testTimeoutMs);
triggerText(table, "c");
assertSelectedIndex(table, 2);
sleep(testTimeoutMs);
triggerText(table, "d");
assertSelectedIndex(table, 3);
sleep(testTimeoutMs);
triggerText(table, "b");
assertSelectedIndex(table, 1);
}
@Test
public void testFilenameAutoLookup_InList() throws Exception {
// dir file names start with 'a_...', 'b_...', etc
TestFiles files = createAlphabeticMixedDirectory();
showMultiSelectionChooser(files.parent, FILES_ONLY);
setListMode();
DirectoryList list = getListView();
int testTimeoutMs = 100;
list.setAutoLookupTimeout(testTimeoutMs);
selectFile(list, 0);
focus(list);
triggerText(list, "b");
assertSelectedIndex(list, 1);
sleep(testTimeoutMs);
triggerText(list, "c");
assertSelectedIndex(list, 2);
sleep(testTimeoutMs);
triggerText(list, "d");
assertSelectedIndex(list, 3);
sleep(testTimeoutMs);
triggerText(list, "b");
assertSelectedIndex(list, 1);
}
@Test
public void testFilenameAutoLookup_InList_SimilarNames() throws Exception {
// dir file names start with 'dir1', 'dir1', 'file1...', 'file2...', etc
TestFiles files = createMixedDirectory();
showMultiSelectionChooser(files.parent, FILES_ONLY);
DirectoryList list = getListView();
int testTimeoutMs = 100;
list.setAutoLookupTimeout(testTimeoutMs);
setListMode();
selectFile(list, 0);
focus(list);
triggerText(list, "d");
assertSelectedIndex(list, 1);
sleep(testTimeoutMs);
triggerText(list, "d");
assertSelectedIndex(list, 2);
sleep(testTimeoutMs);
triggerText(list, "f");
assertSelectedIndex(list, 3);
sleep(testTimeoutMs);
triggerText(list, "f");
assertSelectedIndex(list, 4);
sleep(testTimeoutMs);
triggerText(list, "d");
assertSelectedIndex(list, 0);
}
@Test
public void testGetSelectedFiles_FileOnlyMode_FileSelected() throws Exception {
@ -1778,6 +1840,71 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
// Private Methods
//==================================================================================================
private List<File> getExistingFiles(File dir, int count) {
assertTrue(dir.isDirectory());
File[] files = dir.listFiles(f -> f.isFile());
assertTrue("Dir does not contain enough files - '" + dir + "'; count = " + count,
files.length >= count);
// create some consistency between runs
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
List<File> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(files[i]);
}
return result;
}
private boolean containsRecentFile(File file) {
@SuppressWarnings("unchecked")
List<RecentGhidraFile> recents =
(List<RecentGhidraFile>) getInstanceField("recentList", chooser);
for (RecentGhidraFile recent : recents) {
File actual = recent.getAbsoluteFile();
if (file.equals(actual)) {
return true;
}
}
return false;
}
private ActionContext createDirListContext() {
DirectoryList dirlist = getListView();
return new ActionContext(null, dirlist);
}
private boolean isEnabled(DockingAction action, ActionContext context) {
return runSwing(() -> action.isEnabledForContext(context));
}
private void assertSelectedIndex(DirectoryList list, int expected) {
int actual = runSwing(() -> list.getSelectedIndex());
assertEquals("Wrong list index selected", expected, actual);
}
private void assertSelectedIndex(GTable table, int expected) {
int actual = runSwing(() -> table.getSelectedRow());
assertEquals("Wrong table row selected", expected, actual);
}
private void selectFile(DirectoryList list, int index) {
runSwing(() -> list.setSelectedIndex(index));
}
private void selectFile(GTable table, int index) {
runSwing(() -> table.getSelectionModel().setSelectionInterval(index, index));
}
private void focus(Component c) {
runSwing(() -> c.requestFocus());
waitForSwing();
}
private void setFile(File file) throws Exception {
setFile(file, true);
}
@ -1800,7 +1927,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
private void selectFiles(Iterable<File> files) {
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
runSwing(() -> dirlist.setSelectedFiles(files));
}
@ -1818,6 +1945,26 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
waitForSwing();
}
private void setTableMode() {
AbstractButton button = (AbstractButton) findComponentByName(chooser.getComponent(),
"DETAILS_BUTTON");
boolean isSelected = runSwing(() -> button.isSelected());
if (!isSelected) {
// toggle from the table 'details mode'
pressDetailsButton();
}
}
private void setListMode() {
AbstractButton button = (AbstractButton) findComponentByName(chooser.getComponent(),
"DETAILS_BUTTON");
boolean isSelected = runSwing(() -> button.isSelected());
if (isSelected) {
// toggle from the table 'details mode'
pressDetailsButton();
}
}
private void pressDetailsButton() {
pressButtonByName(chooser.getComponent(), "DETAILS_BUTTON");
waitForSwing();
@ -2179,6 +2326,12 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
return file;
}
private File myCreateTempFileWithPrefix(File parent, String prefix) throws IOException {
File file = File.createTempFile(prefix + '_' + getName(), null, parent);
file.deleteOnExit();
return file;
}
private File myCreateTempDirectory(File parent, String name) throws IOException {
File userDir = new File(parent, name);
@ -2224,7 +2377,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
" and current value: " + editorField.getText());
}
assertTrue(!tempfile.exists());
assertFalse(tempfile.exists());
File newTempFile = new File(tempfile.getParentFile(), name);
newTempFile.deleteOnExit();
assertTrue("New file was not created after a rename: " + newTempFile, newTempFile.exists());
@ -2236,7 +2389,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
assertEquals(name, selectedFile.getName());
}
else {
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
File selectedFile = getSelectedFile(dirlist, DEFAULT_TIMEOUT_MILLIS);
assertEquals(name, selectedFile.getName());
}
@ -2341,7 +2494,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
private void assertChooserListContains(File expected) {
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
ListModel<?> model = dirlist.getModel();
int size = model.getSize();
for (int i = 0; i < size; i++) {
@ -2381,10 +2534,14 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
return dirmodel.getFile(selectedIndex);
}
private DirectoryList getDirectoryListViewOfFileChooser() {
private DirectoryList getListView() {
return (DirectoryList) findComponentByName(chooser.getComponent(), "LIST");
}
private GTable getTableView() {
return (GTable) findComponentByName(chooser.getComponent(), "TABLE");
}
private void debugChooser() {
Msg.debug(this, "Current file chooser state: ");
@ -2402,7 +2559,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
// files loaded in the table and list
Msg.debug(this, "\ttable contents: ");
JTable dirtable = (JTable) findComponentByName(chooser.getComponent(), "TABLE");
JTable dirtable = getTableView();
DirectoryTableModel tableModel = (DirectoryTableModel) dirtable.getModel();
int size = tableModel.getRowCount();
for (int i = 0; i < size; i++) {
@ -2410,7 +2567,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
}
Msg.debug(this, "\tlist contents: ");
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
DirectoryList dirlist = getListView();
ListModel<?> model = dirlist.getModel();
size = model.getSize();
for (int i = 0; i < size; i++) {
@ -2455,9 +2612,29 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
File subdir1 = myCreateTempDirectory(dir, "dir1");
File subdir2 = myCreateTempDirectory(dir, "dir2");
File subdir3 = myCreateTempDirectory(dir, "dir3");
File file1 = myCreateTempFile(dir, "file1");
File file2 = myCreateTempFile(dir, "file2");
File file3 = myCreateTempFile(dir, "file3");
File file1 = myCreateTempFileWithPrefix(dir, "file1");
File file2 = myCreateTempFileWithPrefix(dir, "file2");
File file3 = myCreateTempFileWithPrefix(dir, "file3");
files.parent = dir;
files.addDirs(subdir1, subdir2, subdir3);
files.addFiles(file1, file2, file3);
return files;
}
/** Create a temp dir that contains multiple temp dirs and files */
private TestFiles createAlphabeticMixedDirectory() throws IOException {
File dir = createTempDirectory("MixedDir");
TestFiles files = new TestFiles(dir);
File subdir1 = myCreateTempDirectory(dir, "a_dir1");
File subdir2 = myCreateTempDirectory(dir, "b_dir2");
File subdir3 = myCreateTempDirectory(dir, "c_dir3");
File file1 = myCreateTempFileWithPrefix(dir, "d_file1");
File file2 = myCreateTempFileWithPrefix(dir, "e_file2");
File file3 = myCreateTempFileWithPrefix(dir, "f_file3");
files.parent = dir;
files.addDirs(subdir1, subdir2, subdir3);

View File

@ -455,7 +455,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
triggerText(table, "si");
assertEquals(6, table.getSelectedRow());
sleep(GTable.KEY_TIMEOUT);
sleep(GTable.AUTO_LOOKUP_TIMEOUT);
// try again with the sort in the other direction
selectFirstRow();