mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 10:49:34 +02:00
GT-3396 - File Chooser - review fixes
This commit is contained in:
parent
171914f49e
commit
6918ef7b45
13 changed files with 797 additions and 466 deletions
|
@ -0,0 +1,316 @@
|
|||
/* ###
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (isSorted(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;
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = when;
|
||||
}
|
||||
|
||||
void setFoundMatch(boolean foundMatch) {
|
||||
foundPreviousMatch = foundMatch;
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
boolean shouldSkip() {
|
||||
return skip;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,12 +18,10 @@
|
|||
*/
|
||||
package docking.widgets.filechooser;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
|
@ -47,10 +45,6 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
private JTextField listEditorField;
|
||||
private JPanel listEditor;
|
||||
|
||||
private long keyTimeout = AUTO_LOOKUP_TIMEOUT;
|
||||
private long lastLookupTime;
|
||||
private String lastLookupText;
|
||||
|
||||
/** The file being edited */
|
||||
private File editedFile;
|
||||
|
||||
|
@ -119,20 +113,7 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
e.consume();
|
||||
handleEnterKey();
|
||||
return;
|
||||
}
|
||||
|
||||
String eventChar = Character.toString(e.getKeyChar());
|
||||
long when = e.getWhen();
|
||||
if (when - lastLookupTime > keyTimeout) {
|
||||
lastLookupText = eventChar;
|
||||
}
|
||||
else {
|
||||
lastLookupText += eventChar;
|
||||
}
|
||||
|
||||
lastLookupTime = when;
|
||||
lookupText(lastLookupText);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -210,75 +191,6 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
add(listEditor);
|
||||
}
|
||||
|
||||
private void lookupText(String text) {
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int row = getSelectedIndex();
|
||||
int rows = getModel().getSize();
|
||||
if (row >= 0 && row < rows - 1) {
|
||||
if (text.length() == 1) {
|
||||
// fresh search; ignore the current row, could be from a previous match
|
||||
++row;
|
||||
}
|
||||
|
||||
File file = getModel().getElementAt(row);
|
||||
String name = chooser.getDisplayName(file);
|
||||
if (!name.isEmpty() && startsWithIgnoreCase(name, text)) {
|
||||
setSelectedFile(getFile(row));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int index = autoLookupBinary(text);
|
||||
if (index >= 0) {
|
||||
setSelectedFile(getFile(index));
|
||||
}
|
||||
}
|
||||
|
||||
private int autoLookupBinary(String text) {
|
||||
|
||||
// caveat: for this search to work, the data must be ascending sorted
|
||||
List<File> files = model.getAllFiles();
|
||||
File key = new File(lastLookupText);
|
||||
Comparator<File> comparator = (f1, f2) -> {
|
||||
String n1 = chooser.getDisplayName(f1);
|
||||
return compareIgnoreCase(n1, text);
|
||||
};
|
||||
|
||||
int index = Collections.binarySearch(files, key, comparator);
|
||||
if (index < 0) {
|
||||
index = -index - 1;
|
||||
}
|
||||
|
||||
File file = files.get(index);
|
||||
String name = chooser.getDisplayName(file);
|
||||
if (startsWithIgnoreCase(name, text)) {
|
||||
return index;
|
||||
}
|
||||
|
||||
int before = index - 1;
|
||||
if (before >= 0) {
|
||||
file = files.get(before);
|
||||
name = chooser.getDisplayName(file);
|
||||
if (startsWithIgnoreCase(name, text)) {
|
||||
return before;
|
||||
}
|
||||
}
|
||||
|
||||
int after = index + 1;
|
||||
if (after < files.size()) {
|
||||
file = files.get(after);
|
||||
name = chooser.getDisplayName(file);
|
||||
if (startsWithIgnoreCase(name, text)) {
|
||||
return after;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void handleEnterKey() {
|
||||
|
||||
int[] selectedIndices = getSelectedIndices();
|
||||
|
@ -388,17 +300,6 @@ class DirectoryList extends GList<File> implements GhidraFileChooserDirectoryMod
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
|
||||
* @param timeout the timeout
|
||||
* @see #AUTO_LOOKUP_TIMEOUT
|
||||
*/
|
||||
public void setAutoLookupTimeout(long timeout) {
|
||||
keyTimeout = timeout;
|
||||
lastLookupText = null;
|
||||
lastLookupTime = 0;
|
||||
}
|
||||
|
||||
void setSelectedFiles(Iterable<File> files) {
|
||||
|
||||
List<Integer> indexes = new ArrayList<>();
|
||||
|
|
|
@ -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);
|
||||
|
@ -1140,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));
|
||||
}
|
||||
|
@ -1172,6 +1196,10 @@ public class GhidraFileChooser extends DialogComponentProvider
|
|||
}
|
||||
|
||||
private void doSetSelectedFileAndUpdateDisplay(File file) {
|
||||
if (lastInputFocus != null) {
|
||||
lastInputFocus.requestFocusInWindow();
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -1327,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);
|
||||
}
|
||||
|
@ -1350,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' */
|
||||
|
@ -1429,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;
|
||||
|
@ -1851,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
|
||||
//==================================================================================================
|
||||
|
@ -2105,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 AUTO_LOOKUP_TIMEOUT = GTable.AUTO_LOOKUP_TIMEOUT;
|
||||
private GListAutoLookup<T> autoLookup = new GListAutoLookup<T>(this);
|
||||
|
||||
/**
|
||||
* Constructs a <code>GhidraList</code> with an empty model.
|
||||
|
@ -86,5 +88,28 @@ public class GList<T> extends JList<T> implements GComponent {
|
|||
GComponent.setHTMLRenderingFlag((JComponent) getCellRenderer(), false);
|
||||
}
|
||||
addListSelectionListener(e -> ensureIndexIsVisible(getSelectedIndex()));
|
||||
|
||||
addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyReleased(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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -29,12 +29,11 @@ import javax.swing.*;
|
|||
import javax.swing.event.*;
|
||||
import javax.swing.table.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import docking.*;
|
||||
import docking.action.*;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.actions.ToolActions;
|
||||
import docking.widgets.AutoLookup;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.dialogs.SettingsDialog;
|
||||
import docking.widgets.filechooser.GhidraFileChooser;
|
||||
|
@ -84,15 +83,11 @@ public class GTable extends JTable {
|
|||
private static final String LAST_EXPORT_FILE = "LAST_EXPORT_DIR";
|
||||
private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke("ESCAPE");
|
||||
|
||||
public static final long AUTO_LOOKUP_TIMEOUT = 800;
|
||||
private static final int AUTO_LOOKUP_MAX_SEARCH_ROWS = 50000;
|
||||
private long keyTimeout = AUTO_LOOKUP_TIMEOUT;
|
||||
|
||||
private boolean isInitialized;
|
||||
private boolean enableActionKeyBindings;
|
||||
private KeyListener autoLookupListener;
|
||||
private AutoLookupResult lastLookup;
|
||||
private int lookupColumn = -1;
|
||||
|
||||
private GTableAutoLookup autoLookup = new GTableAutoLookup(this);
|
||||
|
||||
/** A list of default renderers created by this table */
|
||||
protected List<TableCellRenderer> defaultGTableRendererList = new ArrayList<>();
|
||||
|
@ -276,144 +271,14 @@ public class GTable extends JTable {
|
|||
}
|
||||
}
|
||||
|
||||
private String getValueString(int row, int col) {
|
||||
TableCellRenderer renderer = getCellRenderer(row, col);
|
||||
if (renderer instanceof JLabel) {
|
||||
prepareRenderer(renderer, row, col);
|
||||
return ((JLabel) renderer).getText();
|
||||
}
|
||||
|
||||
Object obj = getValueAt(row, col);
|
||||
return obj == null ? null : obj.toString();
|
||||
}
|
||||
|
||||
private int lookupText(String text) {
|
||||
if (text == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int row = getSelectedRow();
|
||||
if (row >= 0 && row < getRowCount() - 1) {
|
||||
if (text.length() == 1) {
|
||||
// fresh search; ignore the current row, could be from a previous match
|
||||
++row;
|
||||
}
|
||||
|
||||
int col = convertColumnIndexToView(lookupColumn);
|
||||
if (textMatches(text, row, col)) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataModel instanceof SortedTableModel) {
|
||||
SortedTableModel sortedModel = (SortedTableModel) dataModel;
|
||||
if (lookupColumn == sortedModel.getPrimarySortColumnIndex()) {
|
||||
return autoLookupBinary(sortedModel, text);
|
||||
}
|
||||
}
|
||||
return autoLookupLinear(text);
|
||||
}
|
||||
|
||||
private boolean textMatches(String text, int row, int col) {
|
||||
String value = getValueString(row, col);
|
||||
return StringUtils.startsWithIgnoreCase(value, text);
|
||||
}
|
||||
|
||||
private int autoLookupLinear(String text) {
|
||||
int max = AUTO_LOOKUP_MAX_SEARCH_ROWS;
|
||||
int rows = getRowCount();
|
||||
int start = getSelectedRow();
|
||||
int counter = 0;
|
||||
int col = convertColumnIndexToView(lookupColumn);
|
||||
|
||||
// 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(SortedTableModel model, String text) {
|
||||
|
||||
int index = binarySearch(model, text);
|
||||
int col = convertColumnIndexToView(lookupColumn);
|
||||
if (textMatches(text, index, col)) {
|
||||
return index;
|
||||
}
|
||||
if (index - 1 >= 0) {
|
||||
if (textMatches(text, index - 1, col)) {
|
||||
return index - 1;
|
||||
}
|
||||
}
|
||||
if (index + 1 < model.getRowCount()) {
|
||||
if (textMatches(text, index + 1, col)) {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int binarySearch(SortedTableModel model, String text) {
|
||||
|
||||
int sortedOrder = 1;
|
||||
int primarySortColumnIndex = model.getPrimarySortColumnIndex();
|
||||
TableSortState columnSortState = model.getTableSortState();
|
||||
ColumnSortState sortState = columnSortState.getColumnSortState(primarySortColumnIndex);
|
||||
|
||||
// if sorted descending, then reverse the search direction and change the lookup text to
|
||||
// so that a match will come after the range we seek, which is before the desired text
|
||||
// when sorted in reverse
|
||||
if (!sortState.isAscending()) {
|
||||
sortedOrder = -1;
|
||||
int lastPos = text.length() - 1;
|
||||
char lastChar = text.charAt(lastPos);
|
||||
++lastChar;
|
||||
text = text.substring(0, lastPos) + lastChar;
|
||||
}
|
||||
|
||||
int min = 0;
|
||||
int rows = model.getRowCount();
|
||||
int max = rows - 1;
|
||||
int col = convertColumnIndexToView(lookupColumn);
|
||||
while (min < max) {
|
||||
int mid = (min + max) / 2;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the delay between keystrokes after which each keystroke is considered a new lookup
|
||||
* @param timeout the timeout
|
||||
* @see #setAutoLookupColumn(int)
|
||||
* @see #AUTO_LOOKUP_TIMEOUT
|
||||
* @see AutoLookup#KEY_TYPING_TIMEOUT
|
||||
*/
|
||||
public void setAutoLookupTimeout(long timeout) {
|
||||
keyTimeout = timeout;
|
||||
lastLookup = null;
|
||||
autoLookup.setTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -426,7 +291,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() {
|
||||
|
@ -441,25 +306,7 @@ public class GTable extends JTable {
|
|||
return;
|
||||
}
|
||||
|
||||
AutoLookupResult lookup = lastLookup;
|
||||
if (lookup == null) {
|
||||
lookup = new AutoLookupResult();
|
||||
}
|
||||
|
||||
lookup.keyTyped(e);
|
||||
if (lookup.shouldSkip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int row = lookupText(lookup.getText());
|
||||
lookup.setFoundMatch(row >= 0);
|
||||
if (row >= 0) {
|
||||
setRowSelectionInterval(row, row);
|
||||
Rectangle rect = getCellRect(row, 0, false);
|
||||
scrollRectToVisible(rect);
|
||||
}
|
||||
|
||||
lastLookup = lookup;
|
||||
autoLookup.keyTyped(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -473,30 +320,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
|
||||
|
@ -1575,50 +1398,4 @@ public class GTable extends JTable {
|
|||
return sourceComponent instanceof GTable;
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoLookupResult {
|
||||
private long lastTime;
|
||||
private String text;
|
||||
private boolean foundPreviousMatch;
|
||||
private boolean skip;
|
||||
|
||||
public void keyTyped(KeyEvent e) {
|
||||
skip = false;
|
||||
|
||||
if (isIgnorableKeyEvent(e)) {
|
||||
skip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
String eventChar = Character.toString(e.getKeyChar());
|
||||
long when = e.getWhen();
|
||||
if (when - lastTime > keyTimeout) {
|
||||
text = eventChar;
|
||||
}
|
||||
else {
|
||||
text += eventChar;
|
||||
|
||||
if (!foundPreviousMatch) {
|
||||
// The given character is being added to the previous search. If that search
|
||||
// was fruitless, then so too will be this one, since we use a
|
||||
// 'starts with' match.
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = when;
|
||||
}
|
||||
|
||||
void setFoundMatch(boolean foundMatch) {
|
||||
foundPreviousMatch = foundMatch;
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
boolean shouldSkip() {
|
||||
return skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue