GP-3492 - File Chooser - Allow users to edit the path field

This commit is contained in:
dragonmacher 2024-11-13 17:27:49 -05:00
parent 2928750c96
commit e56c279b7b
10 changed files with 257 additions and 36 deletions

View File

@ -402,7 +402,7 @@ public abstract class AbstractGhidraHeadedDebuggerTest
/**
* Only use this to escape from pop-up menus. Otherwise, use
* {@link #triggerEscapeKey(Component)}.
* {@link #triggerEscape(Component)}.
*
* @throws AWTException
*/

View File

@ -4,9 +4,9 @@
* 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.
@ -401,7 +401,7 @@ public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest
waitForTasks();
modelProvider.pathField.setText("SomeNonsenseToBeCancelled");
triggerEscapeKey(modelProvider.pathField);
triggerEscape(modelProvider.pathField);
waitForSwing();
assertPathIsThreadsContainer();

View File

@ -4,9 +4,9 @@
* 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.
@ -195,14 +195,14 @@ public abstract class AbstractListingMergeManagerTest extends AbstractMergeTest
KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
assertNotNull(activeWindow);
waitForSwing();
triggerEscapeKey(activeWindow);
triggerEscape(activeWindow);
}
void escapeWindowWithTitleContaining(String partOfTitle) {
Window win = getWindowWithTitleContaining(partOfTitle);
if (win != null) {
waitForSwing();
triggerEscapeKey(win);
triggerEscape(win);
}
}

View File

@ -4,9 +4,9 @@
* 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.
@ -772,7 +772,7 @@ public class FrontEndPluginActionsTest extends AbstractGhidraHeadedIntegrationTe
private void clearText(Component c, String text) {
int n = text.length();
for (int i = 0; i < n; i++) {
triggerBackspaceKey(c);
triggerBackspace(c);
}
}

View File

@ -56,6 +56,8 @@ import utilities.util.reflection.ReflectionUtilities;
* @since Tracker Id 329
*/
public class KeyBindingUtils {
private static final String NO_KEYBINDING_NAME = "none";
private static final String LAST_KEY_BINDING_EXPORT_DIRECTORY = "LastKeyBindingExportDirectory";
private static final String RELEASED = "released";
@ -338,7 +340,7 @@ public class KeyBindingUtils {
}
Object keyText = im.get(keyStroke);
if (keyText == null) {
if (keyText == null || keyText.equals(NO_KEYBINDING_NAME)) {
// no binding--just pick a name
keyText = action.getValue(Action.NAME);
if (keyText == null) {
@ -404,7 +406,7 @@ public class KeyBindingUtils {
int focusCondition) {
InputMap inputMap = component.getInputMap(focusCondition);
if (inputMap != null) {
inputMap.put(keyStroke, "none");
inputMap.put(keyStroke, NO_KEYBINDING_NAME);
}
}

View File

@ -1503,7 +1503,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
triggerKey(destination, modifiers, keyCode, keyChar);
}
public static void triggerEscapeKey(Component c) {
public static void triggerEscape(Component c) {
// text components will not perform built-in actions if they are not focused
if (c instanceof JTextComponent) {
triggerFocusGained(c);
@ -1511,7 +1511,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
triggerText(c, "\033");
}
public static void triggerBackspaceKey(Component c) {
public static void triggerBackspace(Component c) {
triggerText(c, "\010");
}

View File

@ -29,15 +29,19 @@ import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.filechooser.FileSystemView;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import org.apache.commons.lang3.StringUtils;
import docking.*;
import docking.actions.KeyBindingUtils;
import docking.widgets.*;
import docking.widgets.combobox.GComboBox;
import docking.widgets.label.GDLabel;
import docking.widgets.label.GLabel;
import docking.widgets.list.GListCellRenderer;
import docking.widgets.textfield.GFormattedTextField;
import generic.theme.GColor;
import generic.theme.GIcon;
import ghidra.framework.preferences.Preferences;
@ -171,7 +175,7 @@ public class GhidraFileChooser extends ReusableDialogComponentProvider implement
private FileChooserToggleButton downloadsButton;
private FileChooserToggleButton recentButton;
private JTextField currentPathTextField;
private GFormattedTextField currentPathTextField;
private DropDownSelectionTextField<File> filenameTextField;
private DirectoryTableModel directoryTableModel;
private DirectoryTable directoryTable;
@ -378,13 +382,13 @@ public class GhidraFileChooser extends ReusableDialogComponentProvider implement
@Override
public void editingStopped(ChangeEvent e) {
// the user has cancelled editing in the text field (i.e., they pressed ESCAPE)
// the user has cancelled editing in the text field (i.e., they pressed ENTER)
enterCallback();
}
@Override
public void editingCanceled(ChangeEvent e) {
// the user has committed editing from the text field (i.e, they pressed ENTER)
// the user has committed editing from the text field (i.e, they pressed ESCAPE)
escapeCallback();
}
});
@ -460,14 +464,106 @@ public class GhidraFileChooser extends ReusableDialogComponentProvider implement
gbc.gridx = afterPathLabel;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
currentPathTextField = new JTextField();
currentPathTextField.setName("Path");
currentPathTextField.setEditable(false);
currentPathTextField = buildPathTextField();
headerPanel.add(currentPathTextField, gbc);
return headerPanel;
}
private GFormattedTextField buildPathTextField() {
DefaultFormatter formatter = new DefaultFormatter();
formatter.setOverwriteMode(false);
DefaultFormatterFactory factory = new DefaultFormatterFactory(formatter);
GFormattedTextField textField = new GFormattedTextField(factory, "") {
@Override
public void setText(String t) {
super.setText(t);
setDefaultValue(t);
}
};
textField.setName("Path");
textField.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
lastInputFocus = textField;
}
});
DockingUtils.installUndoRedo(textField);
// have the Escape key clear any edits to the field
KeyStroke escapeKs = KeyBindingUtils.parseKeyStroke("Escape");
Action escapeAction = new AbstractAction("Reset Path") {
@Override
public void actionPerformed(ActionEvent e) {
if (textField.isChanged()) {
textField.reset();
}
else {
// When not edited, pass the event up to the chooser so the behavior works as
// it does elsewhere in the dialog.
escapeCallback();
}
}
};
// remove the table's escape key binding and then add our own
KeyBindingUtils.clearKeyBinding(textField, escapeKs);
KeyBindingUtils.registerAction(textField, escapeKs, escapeAction,
JComponent.WHEN_FOCUSED);
// update Enter to allow the user to pick the selected language
KeyStroke enterKs = KeyBindingUtils.parseKeyStroke("Enter");
Action enterAction = new AbstractAction("Choose File") {
@Override
public void actionPerformed(ActionEvent e) {
if (!textField.isChanged()) {
// When not edited, pass the event up to the chooser so the behavior works as
// it does elsewhere in the dialog.
enterCallback();
return;
}
if (!textField.isValid()) {
return;
}
String text = textField.getText();
File f = new File(text);
if (f.isFile()) {
setSelectedFile(f);
}
else {
updateDirOnly(f, true);
}
}
};
// remove the table's enter key binding and then add our own
KeyBindingUtils.clearKeyBinding(textField, enterKs);
KeyBindingUtils.registerAction(textField, enterKs, enterAction,
JComponent.WHEN_FOCUSED);
// an input verifier that returns true if the path is an existing file or directory
InputVerifier inputVerifier = new InputVerifier() {
@Override
public boolean verify(JComponent input) {
String text = textField.getText();
File f = new File(text);
if (isSpecialDirectory(f)) {
return true;
}
return f.isFile() || f.isDirectory();
}
};
textField.setInputVerifier(inputVerifier);
return textField;
}
private void buildWaitPanel() {
waitPanel = new JPanel(new BorderLayout());
waitPanel.setBorder(BorderFactory.createLoweredBevelBorder());
@ -753,7 +849,8 @@ public class GhidraFileChooser extends ReusableDialogComponentProvider implement
}
private File currentDirectory() {
String path = currentPathTextField.getText();
// The default text should always be valid, regardless of user edits
String path = currentPathTextField.getDefaultText();
if (path.length() == 0) {
return null;
}

View File

@ -97,6 +97,15 @@ public class GFormattedTextField extends JFormattedTextField {
update();
}
/**
* Returns the default text. This is useful to know what the original text is after the user
* has edited the text.
* @return the default text
*/
public String getDefaultText() {
return defaultText;
}
public void disableFocusEventProcessing() {
ignoreFocusEditChanges = true;
}
@ -165,6 +174,30 @@ public class GFormattedTextField extends JFormattedTextField {
update();
}
/**
* Restores this field to its default text.
*/
public void reset() {
setText(defaultText);
update();
}
/**
* Returns true if the contents of this field do not match the default.
* @return true if the contents of this field do not match the default.
*/
public boolean isChanged() {
return getTextEntryStatus() != Status.UNCHANGED;
}
/**
* Returns true if the contents of this field are invalid, as determined by the InputValidator.
* @return true if the contents of this field are invalid, as determined by the InputValidator.
*/
public boolean isInvalid() {
return getTextEntryStatus() == Status.INVALID;
}
public void editingFinished() {
update();
}
@ -196,7 +229,6 @@ public class GFormattedTextField extends JFormattedTextField {
}
private void update() {
updateStatus();
if (isError) {
setForeground(Colors.FOREGROUND);
@ -231,5 +263,4 @@ public class GFormattedTextField extends JFormattedTextField {
textEntryStatusChanged(currentStatus);
}
}
}

View File

@ -4,9 +4,9 @@
* 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.
@ -50,6 +50,8 @@ import docking.test.AbstractDockingTest;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.SpyDropDownWindowVisibilityListener;
import docking.widgets.table.*;
import docking.widgets.textfield.GFormattedTextField;
import docking.widgets.textfield.GFormattedTextField.Status;
import generic.concurrent.ConcurrentQ;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
@ -367,7 +369,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
// a string long enough to make the pick unique, in case there are similarly named files
String filenameText = prefix.substring(0, 13);
typeTextForTextField(filenameText);
typeTextForFilenameTextField(filenameText);
triggerEnter(getFilenameTextField());
// the following code should be put back if we move to the Enter key press simply closing the
@ -802,7 +804,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
setFilenameFieldText("");// clear any text so that we can trigger the matching window
String desktopText = "test";
typeTextForTextField(desktopText);
typeTextForFilenameTextField(desktopText);
assertDropDownWindowIsShowing(true);
@ -1976,6 +1978,70 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
assertChooserPreference(key, null);
}
@Test
public void testEditPath_Directory() throws Exception {
TestFiles files = createMixedDirectory();
File dir = files.randomDir();
setPathFieldText(dir.getAbsolutePath());
triggerEnter(getPathTextField());
waitForChooser();
File currentDir = getCurrentDirectory();
assertEquals(dir, currentDir);
}
@Test
public void testEditPath_File() throws Exception {
TestFiles files = createMixedDirectory();
File file = files.randomFile();
setPathFieldText(file.getAbsolutePath());
triggerEnter(getPathTextField());
waitForChooser();
File currentDir = getCurrentDirectory();
File parent = file.getParentFile();
assertEquals(parent, currentDir);
File selectedFile = getSelectedFile();
assertEquals(file, selectedFile);
}
@Test
public void testEditPath_InvalidPath_Enter() throws Exception {
File startDir = getCurrentDirectory();
setPathFieldText("/some/fake/path");
triggerEnter(getPathTextField());
waitForChooser();
assertPathFieldIsInvalid();
// no change
File currentDir = getCurrentDirectory();
assertEquals(startDir, currentDir);
}
@Test
public void testEditPath_InvalidPath_Escape() throws Exception {
File startDir = getCurrentDirectory();
setPathFieldText("/some/fake/path");
assertPathFieldIsInvalid();
triggerEscape(getPathTextField());
waitForChooser();
assertPathFieldIsUnchanged();
// no change
File currentDir = getCurrentDirectory();
assertEquals(startDir, currentDir);
}
//==================================================================================================
// Private Methods
//==================================================================================================
@ -2348,7 +2414,7 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
}
}
private void typeTextForTextField(String text) {
private void typeTextForFilenameTextField(String text) {
JTextField textField = getFilenameTextField();
triggerText(textField, text);
waitForSwing();
@ -2375,11 +2441,20 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
}
}
private void setFilenameFieldText(final String text) {
final JTextField textField = getFilenameTextField();
private void setFilenameFieldText(String text) {
JTextField textField = getFilenameTextField();
runSwing(() -> textField.requestFocusInWindow());
runSwing(() -> textField.setText(text));
}
private void setPathFieldText(String text) {
GFormattedTextField textField = getPathTextField();
runSwing(() -> textField.requestFocusInWindow());
runSwing(() -> textField.setText(text));
// note: we cannot call textField.setText() here, as that is overridden to make the new
// text the default value, which affects our test for textField.isChanged().
triggerText(textField, text);
waitForSwing();
}
private String getFilenameFieldText() {
@ -2393,6 +2468,10 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
return textField;
}
private GFormattedTextField getPathTextField() {
return (GFormattedTextField) findComponentByName(chooser, "Path");
}
private void show() throws Exception {
show(true);
}
@ -2743,6 +2822,18 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
fail("File chooser does not in its list have file: " + expected);
}
private void assertPathFieldIsInvalid() {
GFormattedTextField textField = getPathTextField();
Status status = runSwing(() -> textField.getTextEntryStatus());
assertEquals(Status.INVALID, status);
}
private void assertPathFieldIsUnchanged() {
GFormattedTextField textField = getPathTextField();
Status status = runSwing(() -> textField.getTextEntryStatus());
assertEquals(Status.UNCHANGED, status);
}
private void assertChooserHidden() {
assertFalse("The chooser is showing; it should be closed",
runSwing(() -> chooser.isShowing()));

View File

@ -4,9 +4,9 @@
* 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.
@ -99,7 +99,7 @@ public class IntegerTextFieldTest extends AbstractDockingTest {
assertTrue(!field.isHexMode());
triggerText(textField, "x");
assertTrue(field.isHexMode());
triggerBackspaceKey(textField);
triggerBackspace(textField);
assertTrue(!field.isHexMode());
}
@ -200,7 +200,7 @@ public class IntegerTextFieldTest extends AbstractDockingTest {
assertEquals(12, listener.values.get(1));
assertEquals(123, listener.values.get(2));
triggerBackspaceKey(textField);
triggerBackspace(textField);
assertEquals(12, listener.values.get(3));
}