mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 10:49:34 +02:00
Merge remote-tracking branch 'origin/GT-3396-dragonmacher-file-chooser-quick-lookup'
This commit is contained in:
commit
43f766e954
17 changed files with 1249 additions and 413 deletions
|
@ -15,11 +15,12 @@
|
|||
*/
|
||||
package ghidra.app.util.html;
|
||||
|
||||
import ghidra.app.util.html.diff.DataTypeDiffInput;
|
||||
import ghidra.util.StringUtilities;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import ghidra.app.util.html.diff.DataTypeDiffInput;
|
||||
|
||||
public class HTMLDataTypeRepresentationDiffInput implements DataTypeDiffInput {
|
||||
|
||||
private HTMLDataTypeRepresentation source;
|
||||
|
@ -43,6 +44,6 @@ public class HTMLDataTypeRepresentationDiffInput implements DataTypeDiffInput {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return source.getClass().getSimpleName() + '\n' + StringUtilities.toString(lines, ",\n");
|
||||
return source.getClass().getSimpleName() + '\n' + StringUtils.join(lines, ",\n");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -0,0 +1,328 @@
|
|||
/* ###
|
||||
* 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;
|
||||
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* A class that holds the logic and state for finding matching rows in a widget when a user types
|
||||
* in the widget. This class was designed for row-based widgets, such as tables and lists.
|
||||
*/
|
||||
public abstract class AutoLookup {
|
||||
|
||||
public static final long KEY_TYPING_TIMEOUT = 800;
|
||||
private static final int MAX_SEARCH_ROWS = 50000;
|
||||
private long keyTimeout = KEY_TYPING_TIMEOUT;
|
||||
|
||||
private AutoLookupItem lastLookup;
|
||||
|
||||
private int lookupColumn = 0;
|
||||
|
||||
/**
|
||||
* Returns the currently selected row
|
||||
* @return the row
|
||||
*/
|
||||
public abstract int getCurrentRow();
|
||||
|
||||
/**
|
||||
* Returns the total number of rows
|
||||
* @return the row count
|
||||
*/
|
||||
public abstract int getRowCount();
|
||||
|
||||
/**
|
||||
* Returns a string representation of the item at the given row and column. The text
|
||||
* should match what the user sees.
|
||||
*
|
||||
* @param row the row
|
||||
* @param col the column
|
||||
* @return the text
|
||||
*/
|
||||
public abstract String getValueString(int row, int col);
|
||||
|
||||
/**
|
||||
* Returns true if the given column is sorted. This class will use a binary search if the
|
||||
* given column is sorted. Otherwise, a brute-force search will be used.
|
||||
*
|
||||
* @param column the column
|
||||
* @return true if sorted
|
||||
*/
|
||||
public abstract boolean isSorted(int column);
|
||||
|
||||
/**
|
||||
* A method that subclasses can override to affect whether this class uses a binary search
|
||||
* for a particular column
|
||||
* @param column the column
|
||||
* @return true if the binary search algorithm will work on the given column
|
||||
*/
|
||||
protected boolean canBinarySearchColumn(int column) {
|
||||
return isSorted(column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the currently sorted column is sorted ascending. This is used in
|
||||
* conjunction with {@link #isSorted(int)}. If that method returns false, then this method
|
||||
* will not be called.
|
||||
*
|
||||
* @return true if sorted ascending
|
||||
*/
|
||||
public abstract boolean isSortedAscending();
|
||||
|
||||
/**
|
||||
* This method will be called when a match for the call to {@link #keyTyped(KeyEvent)} is
|
||||
* found
|
||||
*
|
||||
* @param row the matching row
|
||||
*/
|
||||
public abstract void matchFound(int row);
|
||||
|
||||
/**
|
||||
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
|
||||
* @param timeout the timeout
|
||||
*/
|
||||
public void setTimeout(long timeout) {
|
||||
keyTimeout = timeout;
|
||||
lastLookup = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column that is searched when a lookup is performed
|
||||
* @param column the column
|
||||
*/
|
||||
public void setColumn(int column) {
|
||||
this.lookupColumn = column;
|
||||
lastLookup = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clients call this method when the user types keys
|
||||
*
|
||||
* @param e the key event
|
||||
*/
|
||||
public void keyTyped(KeyEvent e) {
|
||||
|
||||
if (getRowCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
AutoLookupItem lookup = lastLookup;
|
||||
if (lookup == null) {
|
||||
lookup = new AutoLookupItem();
|
||||
}
|
||||
|
||||
lookup.keyTyped(e);
|
||||
if (lookup.shouldSkip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int row = lookupText(lookup.getText());
|
||||
lookup.setFoundMatch(row >= 0);
|
||||
|
||||
if (row >= 0) {
|
||||
matchFound(row);
|
||||
}
|
||||
|
||||
lastLookup = lookup;
|
||||
}
|
||||
|
||||
private int lookupText(String text) {
|
||||
if (text == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int row = getCurrentRow();
|
||||
if (row >= 0 && row < getRowCount() - 1) {
|
||||
if (text.length() == 1) {
|
||||
// fresh search; ignore the current row, could be from a previous match
|
||||
++row;
|
||||
}
|
||||
|
||||
int col = lookupColumn;
|
||||
if (textMatches(text, row, col)) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
if (canBinarySearchColumn(lookupColumn)) {
|
||||
return autoLookupBinary(text);
|
||||
}
|
||||
return autoLookupLinear(text);
|
||||
}
|
||||
|
||||
private boolean textMatches(String text, int row, int col) {
|
||||
String value = getValueString(row, col);
|
||||
return StringUtils.startsWithIgnoreCase(value, text);
|
||||
}
|
||||
|
||||
private boolean isIgnorableKeyEvent(KeyEvent event) {
|
||||
|
||||
// ignore modified keys, except for SHIFT
|
||||
if (!isUnmodifiedOrShift(event.getModifiersEx())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.isActionKey() || event.getKeyChar() == KeyEvent.CHAR_UNDEFINED ||
|
||||
Character.isISOControl(event.getKeyChar())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isUnmodifiedOrShift(int modifiers) {
|
||||
if (modifiers == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int shift = InputEvent.SHIFT_DOWN_MASK;
|
||||
return (modifiers | shift) != shift;
|
||||
}
|
||||
|
||||
private int autoLookupLinear(String text) {
|
||||
int max = MAX_SEARCH_ROWS;
|
||||
int rows = getRowCount();
|
||||
int start = getCurrentRow();
|
||||
int counter = 0;
|
||||
int col = lookupColumn;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int autoLookupBinary(String text) {
|
||||
|
||||
int index = binarySearch(text);
|
||||
int col = lookupColumn;
|
||||
if (textMatches(text, index, col)) {
|
||||
return index;
|
||||
}
|
||||
if (index - 1 >= 0) {
|
||||
if (textMatches(text, index - 1, col)) {
|
||||
return index - 1;
|
||||
}
|
||||
}
|
||||
if (index + 1 < getRowCount()) {
|
||||
if (textMatches(text, index + 1, col)) {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int binarySearch(String text) {
|
||||
|
||||
int sortedOrder = 1;
|
||||
|
||||
// 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 (!isSortedAscending()) {
|
||||
sortedOrder = -1;
|
||||
int lastPos = text.length() - 1;
|
||||
char lastChar = text.charAt(lastPos);
|
||||
++lastChar;
|
||||
text = text.substring(0, lastPos) + lastChar;
|
||||
}
|
||||
|
||||
int min = 0;
|
||||
int rows = getRowCount();
|
||||
int max = rows - 1;
|
||||
int col = lookupColumn;
|
||||
while (min < max) {
|
||||
|
||||
// divide by 2; preserve the sign to prevent possible overflow issue
|
||||
int mid = (min + max) >>> 1;
|
||||
String value = getValueString(mid, col);
|
||||
int compare = text.compareToIgnoreCase(value);
|
||||
compare *= sortedOrder;
|
||||
|
||||
if (compare < 0) {
|
||||
max = mid - 1;
|
||||
}
|
||||
else if (compare > 0) {
|
||||
min = mid + 1;
|
||||
}
|
||||
else { // exact match
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
private class AutoLookupItem {
|
||||
private long lastTime;
|
||||
private String text;
|
||||
private boolean foundPreviousMatch;
|
||||
private boolean skip;
|
||||
|
||||
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;
|
||||
when = lastTime; // don't save time if no match found; trigger a timeout
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = when;
|
||||
}
|
||||
|
||||
void setFoundMatch(boolean foundMatch) {
|
||||
foundPreviousMatch = foundMatch;
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
boolean shouldSkip() {
|
||||
return skip;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,8 +29,10 @@ import javax.swing.event.ListDataEvent;
|
|||
import javax.swing.event.ListDataListener;
|
||||
|
||||
import docking.event.mouse.GMouseListenerAdapter;
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.label.GDLabel;
|
||||
import docking.widgets.list.GList;
|
||||
import docking.widgets.list.GListAutoLookup;
|
||||
import ghidra.util.exception.AssertException;
|
||||
|
||||
class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryModelIf {
|
||||
|
@ -110,31 +112,9 @@ 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
|
||||
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);
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
e.consume();
|
||||
handleEnterKey();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -213,6 +193,31 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
add(listEditor);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -251,6 +256,16 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
chooser.userSelectedFiles(selectedFiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AutoLookup createAutoLookup() {
|
||||
return new GListAutoLookup<>(this) {
|
||||
@Override
|
||||
protected boolean canBinarySearchColumn(int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSelectedRows() {
|
||||
return getSelectedIndices();
|
||||
|
|
|
@ -29,6 +29,7 @@ import javax.swing.event.ChangeEvent;
|
|||
import javax.swing.table.TableColumn;
|
||||
|
||||
import docking.event.mouse.GMouseListenerAdapter;
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.GenericDateCellRenderer;
|
||||
import docking.widgets.table.*;
|
||||
import utilities.util.FileUtilities;
|
||||
|
@ -49,6 +50,7 @@ class DirectoryTable extends GTable implements GhidraFileChooserDirectoryModelIf
|
|||
|
||||
private void build() {
|
||||
setAutoLookupColumn(DirectoryTableModel.FILE_COL);
|
||||
|
||||
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
setShowGrid(false);
|
||||
|
||||
|
@ -137,6 +139,19 @@ class DirectoryTable extends GTable implements GhidraFileChooserDirectoryModelIf
|
|||
column.setCellRenderer(new GenericDateCellRenderer());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AutoLookup createAutoLookup() {
|
||||
return new GTableAutoLookup(this) {
|
||||
@Override
|
||||
protected boolean canBinarySearchColumn(int column) {
|
||||
if (column == DirectoryTableModel.FILE_COL) {
|
||||
return false;
|
||||
}
|
||||
return super.canBinarySearchColumn(column);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void maybeSelectItem(MouseEvent e) {
|
||||
Point point = e.getPoint();
|
||||
int row = rowAtPoint(point);
|
||||
|
@ -275,5 +290,4 @@ class DirectoryTable extends GTable implements GhidraFileChooserDirectoryModelIf
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,8 +39,7 @@ import docking.widgets.list.GListCellRenderer;
|
|||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.framework.Platform;
|
||||
import ghidra.framework.preferences.Preferences;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.filechooser.*;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
|
@ -142,8 +141,11 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
private static boolean initialized;
|
||||
private static List<RecentGhidraFile> recentList = new ArrayList<>();
|
||||
|
||||
private HistoryList<File> history = new HistoryList<>(20, dir -> {
|
||||
updateDirAndSelectFile(dir, null, false, false);
|
||||
private HistoryList<HistoryEntry> history = new HistoryList<>(20, (files, previous) -> {
|
||||
|
||||
updateHistoryWithSelectedFiles(previous);
|
||||
|
||||
updateDirAndSelectFile(files.parentDir, files.getSelectedFile(), false, false);
|
||||
updateNavigationButtons();
|
||||
});
|
||||
private File initialFile = null;
|
||||
|
@ -196,6 +198,15 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
private boolean multiSelectionEnabled;
|
||||
private FileChooserActionManager actionManager;
|
||||
|
||||
/**
|
||||
* The last input component to take focus (the text field or file view).
|
||||
*
|
||||
* <p>This may annoy users that are using the keyboard to perform navigation operations via
|
||||
* the toolbar buttons, as we will keep putting focus back into the last input item. We
|
||||
* may need a way to set this field to null when the user is working in this fashion.
|
||||
*/
|
||||
private Component lastInputFocus;
|
||||
|
||||
private Worker worker = Worker.createGuiWorker();
|
||||
|
||||
private GFileChooserOptionsDialog optionsDialog = new GFileChooserOptionsDialog();
|
||||
|
@ -376,6 +387,13 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
}
|
||||
});
|
||||
|
||||
filenameTextField.addFocusListener(new FocusAdapter() {
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
lastInputFocus = filenameTextField;
|
||||
}
|
||||
});
|
||||
|
||||
// This is a callback when the user has made a choice from the selection window.
|
||||
selectionListener = new SelectionListener<>();
|
||||
filenameTextField.addDropDownSelectionChoiceListener(selectionListener);
|
||||
|
@ -558,6 +576,13 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
directoryList.setName("LIST");
|
||||
directoryList.setBackground(BACKGROUND_COLOR);
|
||||
|
||||
directoryList.addFocusListener(new FocusAdapter() {
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
lastInputFocus = directoryList;
|
||||
}
|
||||
});
|
||||
|
||||
directoryScroll = new JScrollPane(directoryList);
|
||||
directoryScroll.getViewport().setBackground(BACKGROUND_COLOR);
|
||||
directoryScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
|
||||
|
@ -815,22 +840,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) {
|
||||
|
@ -1141,10 +1165,9 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
// SCR 4513 - exception if we don't cancel edits before changing the display
|
||||
cancelEdits();
|
||||
|
||||
SystemUtilities.runSwingNow(() -> {
|
||||
if (addToHistory) {
|
||||
addToBackHistory(directory);
|
||||
}
|
||||
Swing.runNow(() -> {
|
||||
updateHistory(directory, addToHistory);
|
||||
|
||||
if (directory.equals(MY_COMPUTER) || directory.equals(RECENT)) {
|
||||
currentPathTextField.setText(getFilename(directory));
|
||||
}
|
||||
|
@ -1173,6 +1196,10 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
}
|
||||
|
||||
private void doSetSelectedFileAndUpdateDisplay(File file) {
|
||||
if (lastInputFocus != null) {
|
||||
lastInputFocus.requestFocusInWindow();
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -1328,7 +1355,7 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
* Displays the WAIT panel. It handles the Swing thread issues.
|
||||
*/
|
||||
private void setWaitPanelVisible(final boolean visible) {
|
||||
SystemUtilities.runSwingLater(() -> {
|
||||
Swing.runLater(() -> {
|
||||
if (visible) {
|
||||
card.show(cardPanel, CARD_WAIT);
|
||||
}
|
||||
|
@ -1351,23 +1378,38 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
upLevelButton.setEnabled(enable);
|
||||
}
|
||||
|
||||
private void addToBackHistory(File dir) {
|
||||
private void updateHistoryWithSelectedFiles(HistoryEntry historyEntry) {
|
||||
|
||||
File currentDir = currentDirectory();
|
||||
File selectedFile = selectedFiles.getFile();
|
||||
historyEntry.setSelectedFile(currentDir, selectedFile);
|
||||
}
|
||||
|
||||
private void updateHistory(File dir, boolean addToHistory) {
|
||||
|
||||
if (!directoryExistsOrIsLogicalDirectory(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dir.equals(currentDir)) {
|
||||
return;
|
||||
HistoryEntry historyEntry = history.getCurrentHistoryItem();
|
||||
if (historyEntry != null) {
|
||||
|
||||
updateHistoryWithSelectedFiles(historyEntry);
|
||||
|
||||
if (historyEntry.isSameDir(dir)) {
|
||||
// already recorded in history
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
history.add(dir);
|
||||
updateNavigationButtons();
|
||||
if (addToHistory) {
|
||||
history.add(new HistoryEntry(dir, null));
|
||||
updateNavigationButtons();
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ HistoryList<File> getHistory() {
|
||||
return history;
|
||||
/*package*/ int getHistorySize() {
|
||||
return history.size();
|
||||
}
|
||||
|
||||
/** Returns true if the file exists on disk OR if it is a logical dir, like 'My Computer' */
|
||||
|
@ -1430,6 +1472,13 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
directoryTable.setName("TABLE");
|
||||
directoryTable.setBackground(BACKGROUND_COLOR);
|
||||
|
||||
directoryTable.addFocusListener(new FocusAdapter() {
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
lastInputFocus = directoryTable;
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(directoryTable);
|
||||
scrollPane.getViewport().setBackground(BACKGROUND_COLOR);
|
||||
return scrollPane;
|
||||
|
@ -1852,6 +1901,20 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
return showDetails;
|
||||
}
|
||||
|
||||
String getInvalidFilenameMessage(String filename) {
|
||||
switch (filename) {
|
||||
case ".":
|
||||
case "..":
|
||||
return "Reserved name '" + filename + "'";
|
||||
default:
|
||||
Matcher m = GhidraFileChooser.INVALID_FILENAME_PATTERN.matcher(filename);
|
||||
if (m.find()) {
|
||||
return "Invalid characters: " + m.group();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
@ -2106,19 +2169,57 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
public synchronized File getFile() {
|
||||
return files.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return files.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String getInvalidFilenameMessage(String filename) {
|
||||
switch (filename) {
|
||||
case ".":
|
||||
case "..":
|
||||
return "Reserved name '" + filename + "'";
|
||||
default:
|
||||
Matcher m = GhidraFileChooser.INVALID_FILENAME_PATTERN.matcher(filename);
|
||||
if (m.find()) {
|
||||
return "Invalid characters: " + m.group();
|
||||
}
|
||||
/**
|
||||
* Container class to manage history entries for a directory and any selected file
|
||||
*/
|
||||
private class HistoryEntry {
|
||||
private File parentDir;
|
||||
private File selectedFile;
|
||||
|
||||
HistoryEntry(File parentDir, File selectedFile) {
|
||||
this.parentDir = parentDir;
|
||||
this.selectedFile = selectedFile;
|
||||
}
|
||||
|
||||
boolean isSameDir(File dir) {
|
||||
return dir.equals(parentDir);
|
||||
}
|
||||
|
||||
void setSelectedFile(File dir, File selectedFile) {
|
||||
if (!parentDir.equals(dir)) {
|
||||
// not my dir; don't save the selection
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFile == null) {
|
||||
// nothing to save
|
||||
return;
|
||||
}
|
||||
|
||||
File selectedParent = selectedFile.getParentFile();
|
||||
if (parentDir.equals(selectedParent)) {
|
||||
this.selectedFile = selectedFile;
|
||||
}
|
||||
}
|
||||
|
||||
File getSelectedFile() {
|
||||
return selectedFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String selectedFilesText = "";
|
||||
if (selectedFile != null) {
|
||||
selectedFilesText = " *" + selectedFile;
|
||||
}
|
||||
return parentDir.getName() + selectedFilesText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,15 @@
|
|||
*/
|
||||
package docking.widgets.list;
|
||||
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.Vector;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.Position.Bias;
|
||||
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.GComponent;
|
||||
import docking.widgets.table.GTable;
|
||||
|
||||
/**
|
||||
* A sub-class of JList that provides an auto-lookup feature.
|
||||
|
@ -35,8 +38,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...
|
||||
private AutoLookup autoLookup = createAutoLookup();
|
||||
|
||||
/**
|
||||
* Constructs a <code>GhidraList</code> with an empty model.
|
||||
|
@ -86,5 +88,37 @@ public class GList<T> extends JList<T> implements GComponent {
|
|||
GComponent.setHTMLRenderingFlag((JComponent) getCellRenderer(), false);
|
||||
}
|
||||
addListSelectionListener(e -> ensureIndexIsVisible(getSelectedIndex()));
|
||||
|
||||
addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
autoLookup.keyTyped(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
|
||||
* @param timeout the timeout
|
||||
* @see AutoLookup#KEY_TYPING_TIMEOUT
|
||||
*/
|
||||
public void setAutoLookupTimeout(long timeout) {
|
||||
autoLookup.setTimeout(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextMatch(String prefix, int startIndex, Bias bias) {
|
||||
// disable the default lookup algorithm, as it does not use the renderer, but
|
||||
// only the object's toString(), which will not always match what the user sees on screen
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subclasses to change the type of {@link AutoLookup} created by this list
|
||||
* @return the auto lookup
|
||||
*/
|
||||
protected AutoLookup createAutoLookup() {
|
||||
return new GListAutoLookup<>(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/* ###
|
||||
* 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.list;
|
||||
|
||||
import java.awt.Component;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.ListCellRenderer;
|
||||
|
||||
import docking.widgets.AutoLookup;
|
||||
|
||||
/**
|
||||
* {@link AutoLookup} implementation for {@link GList}s
|
||||
*
|
||||
* @param <T> the row type
|
||||
*/
|
||||
public class GListAutoLookup<T> extends AutoLookup {
|
||||
|
||||
private GList<T> list;
|
||||
|
||||
public GListAutoLookup(GList<T> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentRow() {
|
||||
return list.getSelectedIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return list.getModel().getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueString(int row, int col) {
|
||||
ListCellRenderer<? super T> renderer = list.getCellRenderer();
|
||||
T value = list.getModel().getElementAt(row);
|
||||
if (!(renderer instanceof JLabel)) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
Component c = renderer.getListCellRendererComponent(list, value, row, false, false);
|
||||
return ((JLabel) c).getText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSorted(int column) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSortedAscending() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void matchFound(int row) {
|
||||
list.setSelectedIndex(row);
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,7 @@ import docking.*;
|
|||
import docking.action.*;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.actions.ToolActions;
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.dialogs.SettingsDialog;
|
||||
import docking.widgets.filechooser.GhidraFileChooser;
|
||||
|
@ -80,15 +81,13 @@ 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 int userDefinedRowHeight;
|
||||
private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke("ESCAPE");
|
||||
|
||||
private boolean isInitialized;
|
||||
private boolean enableActionKeyBindings;
|
||||
private KeyListener autoLookupListener;
|
||||
private long lastLookupTime;
|
||||
private String lookupString;
|
||||
private int lookupColumn = -1;
|
||||
|
||||
private AutoLookup autoLookup = createAutoLookup();
|
||||
|
||||
/** A list of default renderers created by this table */
|
||||
protected List<TableCellRenderer> defaultGTableRendererList = new ArrayList<>();
|
||||
|
@ -106,10 +105,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<>();
|
||||
|
||||
|
@ -189,6 +187,14 @@ public class GTable extends JTable {
|
|||
return new GTableColumnModel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subclasses to change the type of {@link AutoLookup} created by this table
|
||||
* @return the auto lookup
|
||||
*/
|
||||
protected AutoLookup createAutoLookup() {
|
||||
return new GTableAutoLookup(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColumnModel(TableColumnModel columnModel) {
|
||||
super.setColumnModel(columnModel);
|
||||
|
@ -273,115 +279,18 @@ public class GTable extends JTable {
|
|||
}
|
||||
}
|
||||
|
||||
private int getRow(TableModel model, String keyString) {
|
||||
if (keyString == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int currRow = getSelectedRow();
|
||||
if (currRow >= 0 && currRow < getRowCount() - 1) {
|
||||
if (keyString.length() == 1) {
|
||||
++currRow;
|
||||
}
|
||||
Object obj = getValueAt(currRow, convertColumnIndexToView(lookupColumn));
|
||||
if (obj != null && obj.toString().toLowerCase().startsWith(keyString.toLowerCase())) {
|
||||
return currRow;
|
||||
}
|
||||
}
|
||||
if (model instanceof SortedTableModel) {
|
||||
SortedTableModel sortedModel = (SortedTableModel) model;
|
||||
if (lookupColumn == sortedModel.getPrimarySortColumnIndex()) {
|
||||
return autoLookupBinary(sortedModel, keyString);
|
||||
}
|
||||
}
|
||||
return autoLookupLinear(keyString);
|
||||
/**
|
||||
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
|
||||
* @param timeout the timeout
|
||||
* @see #setAutoLookupColumn(int)
|
||||
* @see AutoLookup#KEY_TYPING_TIMEOUT
|
||||
*/
|
||||
public void setAutoLookupTimeout(long timeout) {
|
||||
autoLookup.setTimeout(timeout);
|
||||
}
|
||||
|
||||
private int autoLookupLinear(String keyString) {
|
||||
int rowCount = getRowCount();
|
||||
int startRow = 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())) {
|
||||
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())) {
|
||||
return i;
|
||||
}
|
||||
if (counter++ > TableUtils.MAX_SEARCH_ROWS) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int autoLookupBinary(SortedTableModel model, String keyString) {
|
||||
String modifiedLookupString = keyString;
|
||||
|
||||
int sortedOrder = 1;
|
||||
int primarySortColumnIndex = model.getPrimarySortColumnIndex();
|
||||
TableSortState columnSortState = model.getTableSortState();
|
||||
ColumnSortState sortState = columnSortState.getColumnSortState(primarySortColumnIndex);
|
||||
|
||||
if (!sortState.isAscending()) {
|
||||
sortedOrder = -1;
|
||||
int lastCharPos = modifiedLookupString.length() - 1;
|
||||
char lastChar = modifiedLookupString.charAt(lastCharPos);
|
||||
++lastChar;
|
||||
modifiedLookupString = modifiedLookupString.substring(0, lastCharPos) + lastChar;
|
||||
}
|
||||
|
||||
int min = 0;
|
||||
int max = model.getRowCount() - 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());
|
||||
compare *= sortedOrder;
|
||||
|
||||
if (compare < 0) {
|
||||
max = i - 1;
|
||||
}
|
||||
else if (compare > 0) {
|
||||
min = i + 1;
|
||||
}
|
||||
else {//compare == 0, MATCH!
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
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 -1;
|
||||
protected AutoLookup getAutoLookup() {
|
||||
return autoLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -394,7 +303,7 @@ public class GTable extends JTable {
|
|||
* @param lookupColumn the column in which auto-lookup will be enabled
|
||||
*/
|
||||
public void setAutoLookupColumn(int lookupColumn) {
|
||||
this.lookupColumn = lookupColumn;
|
||||
autoLookup.setColumn(convertColumnIndexToView(lookupColumn));
|
||||
|
||||
if (autoLookupListener == null) {
|
||||
autoLookupListener = new KeyAdapter() {
|
||||
|
@ -409,25 +318,7 @@ public class GTable extends JTable {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isIgnorableKeyEvent(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
long when = e.getWhen();
|
||||
if (when - lastLookupTime > KEY_TIMEOUT) {
|
||||
lookupString = "" + e.getKeyChar();
|
||||
}
|
||||
else {
|
||||
lookupString += "" + e.getKeyChar();
|
||||
}
|
||||
|
||||
int row = getRow(dataModel, lookupString);
|
||||
if (row >= 0) {
|
||||
setRowSelectionInterval(row, row);
|
||||
Rectangle rect = getCellRect(row, 0, false);
|
||||
scrollRectToVisible(rect);
|
||||
}
|
||||
lastLookupTime = when;
|
||||
autoLookup.keyTyped(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -441,30 +332,6 @@ public class GTable extends JTable {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isIgnorableKeyEvent(KeyEvent event) {
|
||||
|
||||
// ignore modified keys, except for SHIFT
|
||||
if (!isUnmodifiedOrShift(event.getModifiersEx())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.isActionKey() || event.getKeyChar() == KeyEvent.CHAR_UNDEFINED ||
|
||||
Character.isISOControl(event.getKeyChar())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isUnmodifiedOrShift(int modifiers) {
|
||||
if (modifiers == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int shift = InputEvent.SHIFT_DOWN_MASK;
|
||||
return (modifiers | shift) != shift;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the keyboard actions to pass through this table and up the component hierarchy.
|
||||
* Specifically, passing true to this method allows unmodified keystrokes to work
|
||||
|
@ -698,9 +565,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) {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import java.awt.Rectangle;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
|
||||
import docking.widgets.AutoLookup;
|
||||
|
||||
/**
|
||||
* {@link AutoLookup} implementation for {@link GTable}s
|
||||
*/
|
||||
public class GTableAutoLookup extends AutoLookup {
|
||||
|
||||
private GTable table;
|
||||
|
||||
public GTableAutoLookup(GTable table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentRow() {
|
||||
return table.getSelectedRow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return table.getRowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueString(int row, int col) {
|
||||
TableCellRenderer renderer = table.getCellRenderer(row, col);
|
||||
if (renderer instanceof JLabel) {
|
||||
table.prepareRenderer(renderer, row, col);
|
||||
return ((JLabel) renderer).getText();
|
||||
}
|
||||
|
||||
Object obj = table.getValueAt(row, col);
|
||||
return obj == null ? null : obj.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSorted(int column) {
|
||||
SortedTableModel sortedModel = (SortedTableModel) table.getModel();
|
||||
return column == sortedModel.getPrimarySortColumnIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSortedAscending() {
|
||||
SortedTableModel model = (SortedTableModel) table.getModel();
|
||||
int primarySortColumnIndex = model.getPrimarySortColumnIndex();
|
||||
TableSortState columnSortState = model.getTableSortState();
|
||||
ColumnSortState sortState = columnSortState.getColumnSortState(primarySortColumnIndex);
|
||||
return sortState.isAscending();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void matchFound(int row) {
|
||||
table.setRowSelectionInterval(row, row);
|
||||
Rectangle rect = table.getCellRect(row, 0, false);
|
||||
table.scrollRectToVisible(rect);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
@ -58,7 +59,6 @@ import ghidra.util.filechooser.ExtensionFileFilter;
|
|||
import ghidra.util.filechooser.GhidraFileChooserModel;
|
||||
import ghidra.util.worker.Worker;
|
||||
import util.CollectionUtils;
|
||||
import util.HistoryList;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
public class GhidraFileChooserTest extends AbstractDockingTest {
|
||||
|
@ -122,7 +122,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
setFile(testDir);
|
||||
|
||||
for (int i = files.size() - 1; i >= 0; --i) {
|
||||
pressBackButton();
|
||||
pressBack();
|
||||
File parentFile = files.get(i).getParentFile();
|
||||
waitForChooser();
|
||||
assertEquals(
|
||||
|
@ -147,16 +147,15 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
public void testBackForSCR_3392() throws Exception {
|
||||
File currentDirectory = getCurrentDirectory();
|
||||
|
||||
pressButtonByName(chooser.getComponent(), "MY_COMPUTER_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressMyComputer();
|
||||
|
||||
File newCurrentDirectory = getCurrentDirectory();
|
||||
assertTrue(!currentDirectory.equals(newCurrentDirectory));
|
||||
assertFalse(currentDirectory.equals(newCurrentDirectory));
|
||||
|
||||
JButton backButton = (JButton) getInstanceField("backButton", chooser);
|
||||
assertTrue(backButton.isEnabled());
|
||||
|
||||
pressBackButton();
|
||||
pressBack();
|
||||
waitForChooser();
|
||||
|
||||
newCurrentDirectory = getCurrentDirectory();
|
||||
|
@ -180,7 +179,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);
|
||||
|
@ -189,12 +188,12 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
waitForChooser();
|
||||
|
||||
// back should now go to the 'home' dir
|
||||
pressBackButton();
|
||||
pressBack();
|
||||
waitForChooser();
|
||||
assertEquals("Did not go back to the home directory", homeDir, getCurrentDirectory());
|
||||
|
||||
// finally, go back to the start dir
|
||||
pressBackButton();
|
||||
pressBack();
|
||||
waitForChooser();
|
||||
assertEquals("Did not go back to the start directory", startDir.getParentFile(),
|
||||
getCurrentDirectory());
|
||||
|
@ -244,7 +243,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();
|
||||
|
@ -256,9 +255,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
(JTextField) findComponentByName(chooser.getComponent(), "LIST_EDITOR_FIELD");
|
||||
assertNotNull(editorField);
|
||||
|
||||
// Now, press the "My Computer" button...
|
||||
pressButtonByName(chooser.getComponent(), "MY_COMPUTER_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressMyComputer();
|
||||
|
||||
// Next, double-click the root drive entry
|
||||
DirectoryListModel model = (DirectoryListModel) dirlist.getModel();
|
||||
|
@ -276,7 +273,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
|
||||
|
@ -402,28 +399,27 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
public void testRefresh() throws Exception {
|
||||
setDir(tempdir);
|
||||
|
||||
HistoryList<File> history = chooser.getHistory();
|
||||
int size = history.size();
|
||||
int size = chooser.getHistorySize();
|
||||
|
||||
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();
|
||||
pressRefresh();
|
||||
waitForChooser();
|
||||
|
||||
assertTrue(dirmodel.contains(tempfile));
|
||||
assertTrue(tempfile.delete());
|
||||
pressRefreshButton();
|
||||
pressRefresh();
|
||||
waitForChooser();
|
||||
|
||||
assertTrue(!dirmodel.contains(tempfile));
|
||||
assertFalse(dirmodel.contains(tempfile));
|
||||
|
||||
// verify back stack is not corrupted!!!
|
||||
assertEquals(size, history.size());
|
||||
assertEquals(size, size = chooser.getHistorySize());
|
||||
}
|
||||
|
||||
// refresh was navigating into the selected directory
|
||||
|
@ -434,7 +430,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
pressNewFolderButton();
|
||||
waitForSwing();
|
||||
|
||||
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
|
||||
DirectoryList dirlist = getListView();
|
||||
File newFile = getNewlyCreatedFile(dirlist);
|
||||
waitForFile(newFile, DEFAULT_TIMEOUT_MILLIS);
|
||||
|
||||
|
@ -446,11 +442,11 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
});
|
||||
|
||||
// press refresh
|
||||
pressRefreshButton();
|
||||
pressRefresh();
|
||||
waitForFile(newFile, DEFAULT_TIMEOUT_MILLIS);
|
||||
|
||||
// verify we did not go into the selected directory
|
||||
assertTrue(!newFile.equals(getCurrentDirectory()));
|
||||
assertFalse(newFile.equals(getCurrentDirectory()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -700,7 +696,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
|
||||
|
@ -960,25 +956,25 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
@Test
|
||||
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");
|
||||
DirectoryList dirlist = getListView();
|
||||
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 +983,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 +1000,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 +1017,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);
|
||||
|
@ -1047,8 +1043,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
}
|
||||
|
||||
setMode(DIRECTORIES_ONLY);
|
||||
pressButtonByName(chooser.getComponent(), "RECENT_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressRecent();
|
||||
|
||||
// make a selection
|
||||
File fileToSelect = files.get(0).getParentFile();
|
||||
|
@ -1066,9 +1061,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
// make sure the user cannot type in relative filenames when in a special dir (like
|
||||
// Recent and My Computer)
|
||||
|
||||
// press the recent button
|
||||
pressButtonByName(chooser.getComponent(), "RECENT_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressRecent();
|
||||
|
||||
setFilenameFieldText("foo");
|
||||
|
||||
|
@ -1077,8 +1070,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
assertTrue("The file chooser accepted an invalid file parented by the RECENTs directory",
|
||||
chooser.isShowing());
|
||||
|
||||
pressButtonByName(chooser.getComponent(), "MY_COMPUTER_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressMyComputer();
|
||||
|
||||
setFilenameFieldText("foo");
|
||||
|
||||
|
@ -1103,7 +1095,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 +1145,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 +1317,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,17 +1324,16 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
|
||||
@Test
|
||||
public void testRenameInTable() throws Exception {
|
||||
pressDetailsButton();
|
||||
setTableMode();
|
||||
waitForSwing();
|
||||
doRenameTest(chooser.getActionManager(), "TABLE_EDITOR_FIELD");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMyComputer() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "MY_COMPUTER_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressMyComputer();
|
||||
|
||||
JList<?> dirlist = (JList<?>) findComponentByName(chooser.getComponent(), "LIST");
|
||||
DirectoryList dirlist = getListView();
|
||||
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
|
||||
File[] roots = chooser.getModel().getRoots();
|
||||
assertEquals(roots.length, listModel.getSize());
|
||||
|
@ -1471,7 +1445,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
getCurrentDirectory());
|
||||
|
||||
// check the chooser contents
|
||||
JList<?> dirlist = (JList<?>) findComponentByName(chooser.getComponent(), "LIST");
|
||||
DirectoryList dirlist = getListView();
|
||||
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
|
||||
File[] listing = chooser.getModel().getListing(homeDir, null);
|
||||
assertEquals(listing.length, listModel.getSize());
|
||||
|
@ -1489,10 +1463,9 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
show();
|
||||
}
|
||||
|
||||
pressButtonByName(chooser.getComponent(), "RECENT_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressRecent();
|
||||
|
||||
JList<?> dirlist = (JList<?>) findComponentByName(chooser.getComponent(), "LIST");
|
||||
DirectoryList dirlist = getListView();
|
||||
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
|
||||
for (File element : files) {
|
||||
assertTrue("model does not contain the recent file: " + element.getParentFile(),
|
||||
|
@ -1520,8 +1493,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
show();
|
||||
}
|
||||
|
||||
pressButtonByName(chooser.getComponent(), "RECENT_BUTTON", false);
|
||||
waitForChooser();
|
||||
pressRecent();
|
||||
|
||||
// must re-retrieve the action, since we created a new chooser
|
||||
actionManager = chooser.getActionManager();
|
||||
|
@ -1545,34 +1517,9 @@ 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");
|
||||
DirectoryList dirlist = getListView();
|
||||
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
|
||||
runSwing(() -> chooser.setFileFilter(new ExtensionFileFilter("exe", "Executables")));
|
||||
|
||||
|
@ -1671,6 +1618,145 @@ 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();
|
||||
DirectoryTable 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 testFocus_FilesViewStaysFocusedAfterRefresh() throws Exception {
|
||||
|
||||
DirectoryList list = getListView();
|
||||
focus(list);
|
||||
|
||||
clickMyComputer();
|
||||
assertTrue(list.hasFocus());
|
||||
|
||||
clickRecent();
|
||||
assertTrue(list.hasFocus());
|
||||
|
||||
clickBack();
|
||||
assertTrue(list.hasFocus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHistoryRestoresSelectedFiles() throws Exception {
|
||||
|
||||
File startDir = createTempDir();
|
||||
setDir(startDir);
|
||||
createFileSubFile(startDir, 3);
|
||||
|
||||
pressUp();
|
||||
selectFile(getListView(), 1);
|
||||
|
||||
pressUp();
|
||||
selectFile(getListView(), 2);
|
||||
|
||||
pressBack();
|
||||
assertSelectedIndex(getListView(), 1);
|
||||
|
||||
pressForward();
|
||||
assertSelectedIndex(getListView(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSelectedFiles_FileOnlyMode_FileSelected() throws Exception {
|
||||
|
||||
|
@ -1778,6 +1864,73 @@ 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 File selectFile(DirectoryList list, int index) {
|
||||
runSwing(() -> list.setSelectedIndex(index));
|
||||
return runSwing(() -> list.getSelectedFile());
|
||||
}
|
||||
|
||||
private File selectFile(DirectoryTable table, int index) {
|
||||
runSwing(() -> table.getSelectionModel().setSelectionInterval(index, index));
|
||||
return runSwing(() -> table.getSelectedFile());
|
||||
}
|
||||
|
||||
private void focus(Component c) {
|
||||
runSwing(() -> c.requestFocus());
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
private void setFile(File file) throws Exception {
|
||||
setFile(file, true);
|
||||
}
|
||||
|
@ -1800,7 +1953,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
|
||||
private void selectFiles(Iterable<File> files) {
|
||||
|
||||
DirectoryList dirlist = getDirectoryListViewOfFileChooser();
|
||||
DirectoryList dirlist = getListView();
|
||||
runSwing(() -> dirlist.setSelectedFiles(files));
|
||||
}
|
||||
|
||||
|
@ -1818,11 +1971,62 @@ 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();
|
||||
}
|
||||
|
||||
private void pressMyComputer() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "MY_COMPUTER_BUTTON", false);
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void clickMyComputer() throws Exception {
|
||||
AbstractButton button =
|
||||
(AbstractButton) findComponentByName(chooser.getComponent(), "MY_COMPUTER_BUTTON");
|
||||
leftClick(button, 5, 5);
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void clickRecent() throws Exception {
|
||||
AbstractButton button =
|
||||
(AbstractButton) findComponentByName(chooser.getComponent(), "RECENT_BUTTON");
|
||||
leftClick(button, 5, 5);
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void clickBack() throws Exception {
|
||||
AbstractButton button =
|
||||
(AbstractButton) findComponentByName(chooser.getComponent(), "BACK_BUTTON");
|
||||
leftClick(button, 5, 5);
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void pressRecent() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "RECENT_BUTTON", false);
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void pressHome() {
|
||||
pressButtonByName(chooser.getComponent(), "HOME_BUTTON", false);
|
||||
waitForSwing();
|
||||
|
@ -1838,14 +2042,27 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
waitForSwing();
|
||||
}
|
||||
|
||||
private void pressRefreshButton() {
|
||||
private void pressRefresh() {
|
||||
pressButtonByName(chooser.getComponent(), "REFRESH_BUTTON");
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
private void pressBackButton() {
|
||||
private void pressBack() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "BACK_BUTTON");
|
||||
waitForSwing();
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void pressForward() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "FORWARD_BUTTON");
|
||||
waitForSwing();
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void pressUp() throws Exception {
|
||||
pressButtonByName(chooser.getComponent(), "UP_BUTTON");
|
||||
waitForSwing();
|
||||
waitForChooser();
|
||||
}
|
||||
|
||||
private void setDir(final File dir) throws Exception {
|
||||
|
@ -2179,6 +2396,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 +2447,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 +2459,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 +2564,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 +2604,14 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
|
|||
return dirmodel.getFile(selectedIndex);
|
||||
}
|
||||
|
||||
private DirectoryList getDirectoryListViewOfFileChooser() {
|
||||
private DirectoryList getListView() {
|
||||
return (DirectoryList) findComponentByName(chooser.getComponent(), "LIST");
|
||||
}
|
||||
|
||||
private DirectoryTable getTableView() {
|
||||
return (DirectoryTable) findComponentByName(chooser.getComponent(), "TABLE");
|
||||
}
|
||||
|
||||
private void debugChooser() {
|
||||
Msg.debug(this, "Current file chooser state: ");
|
||||
|
||||
|
@ -2402,7 +2629,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 +2637,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 +2682,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);
|
||||
|
|
|
@ -34,6 +34,7 @@ import javax.swing.event.TableModelEvent;
|
|||
import org.junit.*;
|
||||
|
||||
import docking.DockingUtils;
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.filter.*;
|
||||
import docking.widgets.table.*;
|
||||
import docking.widgets.table.ColumnSortState.SortDirection;
|
||||
|
@ -455,7 +456,7 @@ public class ThreadedTableTest extends AbstractThreadedTableTest {
|
|||
triggerText(table, "si");
|
||||
assertEquals(6, table.getSelectedRow());
|
||||
|
||||
sleep(GTable.KEY_TIMEOUT);
|
||||
sleep(AutoLookup.KEY_TYPING_TIMEOUT);
|
||||
|
||||
// try again with the sort in the other direction
|
||||
selectFirstRow();
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
*/
|
||||
package ghidra.util;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -555,9 +555,9 @@ public class StringUtilities {
|
|||
length *= -1;
|
||||
}
|
||||
|
||||
int numFillers = length - source.length();
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
for (int f = 0; f < numFillers; f++) {
|
||||
int n = length - source.length();
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) {
|
||||
buffer.append(filler);
|
||||
}
|
||||
|
||||
|
@ -756,23 +756,6 @@ public class StringUtilities {
|
|||
return new String(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn the given data into an attractive string, with the separator of your choosing
|
||||
*
|
||||
* @param collection the data from which a string will be generated
|
||||
* @param separator the string used to separate elements
|
||||
* @return a string representation of the given list
|
||||
*/
|
||||
public static String toString(Collection<?> collection, String separator) {
|
||||
if (collection == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String asString =
|
||||
collection.stream().map(o -> o.toString()).collect(Collectors.joining(separator));
|
||||
return "[ " + asString + " ]";
|
||||
}
|
||||
|
||||
public static String toStringWithIndent(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
|
@ -783,19 +766,6 @@ public class StringUtilities {
|
|||
return indented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the characters in the given string
|
||||
*
|
||||
* @param s the string to reverse
|
||||
* @return the reversed string
|
||||
*/
|
||||
public static String reverse(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return new StringBuilder(s).reverse().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two strings into one.
|
||||
* If one string contains the other, then the largest is returned.
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
package util;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.datastruct.FixedSizeStack;
|
||||
|
||||
|
@ -47,7 +50,7 @@ import ghidra.util.datastruct.FixedSizeStack;
|
|||
public class HistoryList<T> {
|
||||
|
||||
private final FixedSizeStack<T> historyStack;
|
||||
private final Consumer<T> itemSelectedCallback;
|
||||
private final BiConsumer<T, T> itemSelectedCallback;
|
||||
private int historyIndex;
|
||||
private boolean isBroadcasting;
|
||||
|
||||
|
@ -64,6 +67,20 @@ public class HistoryList<T> {
|
|||
* going back or forward
|
||||
*/
|
||||
public HistoryList(int size, Consumer<T> itemSelectedCallback) {
|
||||
this(size, asBiConsumer(itemSelectedCallback));
|
||||
}
|
||||
|
||||
/**
|
||||
* The sized passed here limits the size of the list, with the oldest items being dropped
|
||||
* as the list grows. The given callback will be called when {@link #goBack()} or
|
||||
* {@link #goForward()} are called.
|
||||
*
|
||||
* @param size the max number of items to keep in the list
|
||||
* @param itemSelectedCallback the function to call when the client selects an item by
|
||||
* going back or forward. This callback will be passed the newly selected item as
|
||||
* the first argument and the previously selected item as the second argument.
|
||||
*/
|
||||
public HistoryList(int size, BiConsumer<T, T> itemSelectedCallback) {
|
||||
Objects.requireNonNull(itemSelectedCallback, "Item selected callback cannot be null");
|
||||
|
||||
if (size < 1) {
|
||||
|
@ -74,6 +91,10 @@ public class HistoryList<T> {
|
|||
this.historyStack = new FixedSizeStack<>(size);
|
||||
}
|
||||
|
||||
private static <T> BiConsumer<T, T> asBiConsumer(Consumer<T> consumer) {
|
||||
return (t, ignored) -> consumer.accept(t);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Interface Methods
|
||||
//==================================================================================================
|
||||
|
@ -170,9 +191,10 @@ public class HistoryList<T> {
|
|||
return;
|
||||
}
|
||||
|
||||
T leaving = getCurrentHistoryItem();
|
||||
T t = historyStack.get(--historyIndex);
|
||||
dropNull();
|
||||
broadcast(t);
|
||||
broadcast(t, leaving);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -198,8 +220,9 @@ public class HistoryList<T> {
|
|||
return;
|
||||
}
|
||||
|
||||
T leaving = getCurrentHistoryItem();
|
||||
T t = historyStack.get(++historyIndex);
|
||||
broadcast(t);
|
||||
broadcast(t, leaving);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -342,10 +365,10 @@ public class HistoryList<T> {
|
|||
historyStack.remove(itemIndex);
|
||||
}
|
||||
|
||||
private void broadcast(T t) {
|
||||
private void broadcast(T t, T leaving) {
|
||||
try {
|
||||
isBroadcasting = true;
|
||||
itemSelectedCallback.accept(t);
|
||||
itemSelectedCallback.accept(t, leaving);
|
||||
}
|
||||
finally {
|
||||
isBroadcasting = false;
|
||||
|
@ -362,6 +385,9 @@ public class HistoryList<T> {
|
|||
@Override
|
||||
public String toString() {
|
||||
|
||||
String key = " items: ";
|
||||
String newlinePad = StringUtils.repeat(' ', key.length());
|
||||
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
for (int i = 0; i < historyStack.size(); i++) {
|
||||
T t = historyStack.get(i);
|
||||
|
@ -377,13 +403,13 @@ public class HistoryList<T> {
|
|||
}
|
||||
|
||||
if (i != historyStack.size() - 1) {
|
||||
buffy.append(',').append(' ');
|
||||
buffy.append(',').append('\n').append(newlinePad);
|
||||
}
|
||||
}
|
||||
|
||||
//@formatter:off
|
||||
return "{\n" +
|
||||
"\titems: " + buffy.toString() + "\n" +
|
||||
key + buffy.toString() + "\n" +
|
||||
"}";
|
||||
//@formatter:on
|
||||
}
|
||||
|
|
|
@ -248,18 +248,6 @@ public class StringUtilitiesTest {
|
|||
StringUtilities.trimMiddle(overString, max);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReverse() {
|
||||
String hello = "hello";
|
||||
String reversed = StringUtilities.reverse(hello);
|
||||
assertEquals("olleh", reversed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReverseNull() {
|
||||
assertNull(StringUtilities.reverse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLastWord() {
|
||||
assertEquals("word", StringUtilities.getLastWord("/This/is/my/last/word", "/"));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue