diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/html/HTMLDataTypeRepresentationDiffInput.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/html/HTMLDataTypeRepresentationDiffInput.java index 44b3a01758..d731b3f416 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/html/HTMLDataTypeRepresentationDiffInput.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/html/HTMLDataTypeRepresentationDiffInput.java @@ -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"); } } diff --git a/Ghidra/Features/Base/src/test.slow/java/docking/widgets/table/GTableTest.java b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/table/GTableTest.java index b8112d6644..3c2a4d05ce 100644 --- a/Ghidra/Features/Base/src/test.slow/java/docking/widgets/table/GTableTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/table/GTableTest.java @@ -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 { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symtable/SymbolTablePluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symtable/SymbolTablePluginTest.java index ec96d9ea3d..654342d9ed 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symtable/SymbolTablePluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symtable/SymbolTablePluginTest.java @@ -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"); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/AutoLookup.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/AutoLookup.java new file mode 100644 index 0000000000..b7f2b09797 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/AutoLookup.java @@ -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; + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java index b429a37357..98f028a99c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java @@ -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 implements GhidraFileChooserDirectoryModelIf { @@ -110,31 +112,9 @@ class DirectoryList extends GList 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 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 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(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTable.java index c60565593f..3d8a856dd0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTable.java @@ -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 } } - } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java index 3a9d5a40a3..f4220d8d51 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java @@ -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 recentList = new ArrayList<>(); - private HistoryList history = new HistoryList<>(20, dir -> { - updateDirAndSelectFile(dir, null, false, false); + private HistoryList 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). + * + *

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 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 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; } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GList.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GList.java index 6c0f4bda6d..788202891d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GList.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GList.java @@ -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 extends JList 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 GhidraList with an empty model. @@ -86,5 +88,37 @@ public class GList extends JList 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); + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListAutoLookup.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListAutoLookup.java new file mode 100644 index 0000000000..54caa7b456 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListAutoLookup.java @@ -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 the row type + */ +public class GListAutoLookup extends AutoLookup { + + private GList list; + + public GListAutoLookup(GList 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 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); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java index 9a3af2f084..6c4147be1b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java @@ -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 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 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) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableAutoLookup.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableAutoLookup.java new file mode 100644 index 0000000000..f5bd0b4afa --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableAutoLookup.java @@ -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); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java index b76943aed7..c1e4358d13 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java @@ -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. diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java index 5204b269e3..17a14bb5bc 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java @@ -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 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 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 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 recents = - (List) 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 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 result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + result.add(files[i]); + } + return result; + } + + private boolean containsRecentFile(File file) { + + @SuppressWarnings("unchecked") + List recents = + (List) 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 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); diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java index da907dd69c..848c5dd0a7 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/table/threaded/ThreadedTableTest.java @@ -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(); diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java index e50eec8920..5971ce98e5 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java @@ -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. diff --git a/Ghidra/Framework/Generic/src/main/java/util/HistoryList.java b/Ghidra/Framework/Generic/src/main/java/util/HistoryList.java index f279fbf5c5..c1796c6255 100644 --- a/Ghidra/Framework/Generic/src/main/java/util/HistoryList.java +++ b/Ghidra/Framework/Generic/src/main/java/util/HistoryList.java @@ -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 { private final FixedSizeStack historyStack; - private final Consumer itemSelectedCallback; + private final BiConsumer itemSelectedCallback; private int historyIndex; private boolean isBroadcasting; @@ -64,6 +67,20 @@ public class HistoryList { * going back or forward */ public HistoryList(int size, Consumer 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 itemSelectedCallback) { Objects.requireNonNull(itemSelectedCallback, "Item selected callback cannot be null"); if (size < 1) { @@ -74,6 +91,10 @@ public class HistoryList { this.historyStack = new FixedSizeStack<>(size); } + private static BiConsumer asBiConsumer(Consumer consumer) { + return (t, ignored) -> consumer.accept(t); + } + //================================================================================================== // Interface Methods //================================================================================================== @@ -170,9 +191,10 @@ public class HistoryList { return; } + T leaving = getCurrentHistoryItem(); T t = historyStack.get(--historyIndex); dropNull(); - broadcast(t); + broadcast(t, leaving); } /** @@ -198,8 +220,9 @@ public class HistoryList { return; } + T leaving = getCurrentHistoryItem(); T t = historyStack.get(++historyIndex); - broadcast(t); + broadcast(t, leaving); } /** @@ -342,10 +365,10 @@ public class HistoryList { 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 { @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 { } 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 } diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/StringUtilitiesTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/StringUtilitiesTest.java index 4f5fcd6c61..43404a3fc7 100644 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/StringUtilitiesTest.java +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/StringUtilitiesTest.java @@ -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", "/"));