GP-1685 - Fixed the Find Dialog incorrectly matching history entries

when the selected item was programmatically set
This commit is contained in:
dragonmacher 2022-01-20 17:32:20 -05:00
parent e9b4aed8fe
commit f0d0dd532d
7 changed files with 161 additions and 78 deletions

View file

@ -19,7 +19,6 @@ import java.awt.BorderLayout;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.List; import java.util.List;
import java.util.Vector;
import javax.swing.JPanel; import javax.swing.JPanel;
@ -49,18 +48,18 @@ class AnnotationHandlerDialog extends DialogComponentProvider {
addCancelButton(); addCancelButton();
setOkEnabled(true); setOkEnabled(true);
setHelpLocation(new HelpLocation(HelpTopics.DATA_MANAGER, "Export_To")); setHelpLocation(new HelpLocation(HelpTopics.DATA_MANAGER, "Export_To"));
setRememberSize( false ); setRememberSize(false);
} }
@Override @Override
protected void cancelCallback() { protected void cancelCallback() {
close(); close();
} }
@Override @Override
protected void okCallback() { protected void okCallback() {
Object [] objs = handlerComboBox.getSelectedObjects(); Object[] objs = handlerComboBox.getSelectedObjects();
if (objs != null && objs.length > 0) { if (objs != null && objs.length > 0) {
handler = (AnnotationHandler) objs[0]; handler = (AnnotationHandler) objs[0];
} }
@ -71,8 +70,9 @@ class AnnotationHandlerDialog extends DialogComponentProvider {
JPanel create() { JPanel create() {
JPanel outerPanel = new JPanel(new BorderLayout()); JPanel outerPanel = new JPanel(new BorderLayout());
handlerComboBox = new GhidraComboBox<>(new Vector<AnnotationHandler>(handlerList)); handlerComboBox = new GhidraComboBox<>(handlerList);
handlerComboBox.addActionListener(new ActionListener() { handlerComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
okCallback(); okCallback();
} }
@ -81,7 +81,11 @@ class AnnotationHandlerDialog extends DialogComponentProvider {
return outerPanel; return outerPanel;
} }
public AnnotationHandler getHandler() { return handler; } public AnnotationHandler getHandler() {
return handler;
}
public boolean wasSuccessful() { return success; } public boolean wasSuccessful() {
return success;
}
} }

View file

@ -285,7 +285,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
private Component buildFormatChooser() { private Component buildFormatChooser() {
List<Exporter> exporters = getApplicableExporters(); List<Exporter> exporters = getApplicableExporters();
comboBox = new GhidraComboBox<>(new Vector<>(exporters)); comboBox = new GhidraComboBox<>(exporters);
Exporter defaultExporter = getDefaultExporter(exporters); Exporter defaultExporter = getDefaultExporter(exporters);
if (defaultExporter != null) { if (defaultExporter != null) {

View file

@ -260,7 +260,7 @@ public class ImporterDialog extends DialogComponentProvider {
set.add(loader); set.add(loader);
} }
} }
loaderComboBox = new GhidraComboBox<>(new Vector<>(set)); loaderComboBox = new GhidraComboBox<>(set);
loaderComboBox.addItemListener(e -> selectedLoaderChanged()); loaderComboBox.addItemListener(e -> selectedLoaderChanged());
loaderComboBox.setEnterKeyForwarding(true); loaderComboBox.setEnterKeyForwarding(true);
loaderComboBox.setRenderer( loaderComboBox.setRenderer(

View file

@ -45,14 +45,13 @@ public class FindAction extends AbstractDecompilerAction {
if (findDialog == null) { if (findDialog == null) {
findDialog = findDialog =
new FindDialog("Decompiler Find Text", new DecompilerSearcher(decompilerPanel)) { new FindDialog("Decompiler Find Text", new DecompilerSearcher(decompilerPanel)) {
@Override @Override
protected void dialogClosed() { protected void dialogClosed() {
// clear the search results when the dialog is closed // clear the search results when the dialog is closed
decompilerPanel.setSearchResults(null); decompilerPanel.setSearchResults(null);
} }
}; };
findDialog findDialog.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
} }
return findDialog; return findDialog;
} }

View file

@ -17,6 +17,7 @@ package docking.widgets;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.util.List;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
@ -218,9 +219,15 @@ public class FindDialog extends DialogComponentProvider {
public void setSearchText(String text) { public void setSearchText(String text) {
String searchText = text == null ? textField.getText() : text; String searchText = text == null ? textField.getText() : text;
textField.setText(searchText); comboBox.setSelectedItem(searchText);
textField.setSelectionStart(0); }
textField.setSelectionEnd(searchText.length());
public String getSearchText() {
return textField.getText();
}
public void setHistory(List<String> history) {
history.forEach(comboBox::addToModel);
} }
private void storeSearchText(String text) { private void storeSearchText(String text) {

View file

@ -31,30 +31,32 @@ import docking.widgets.GComponent;
/** /**
* GhidraComboBox adds the following features: * GhidraComboBox adds the following features:
* *
* 1) ActionListeners are only invoked when the &lt;Enter&gt; key * <p>
* is pressed within the text-field of the combo-box. * 1) ActionListeners are only invoked when the &lt;Enter&gt; key is pressed within the text-field
* In normal JComboBox case, the ActionListeners are notified * of the combo-box. In normal JComboBox case, the ActionListeners are notified when an item is
* when an item is selected from the list. * selected from the list.
* *
* 2) Adds the auto-completion feature. As a user * <p>
* types in the field, the combo box suggest the nearest matching * 2) Adds the auto-completion feature. As a user types in the field, the combo box suggest the
* entry in the combo box model. * nearest matching entry in the combo box model.
* *
* <p>
* It also fixes the following bug: * It also fixes the following bug:
* *
* A normal JComboBox has a problem (feature?) * <p>
* that if you have a dialog with a button * A normal JComboBox has a problem (feature?) that if you have a dialog with a button and
* and JComboBox and you edit the comboText field and * JComboBox and you edit the comboText field and then hit the button, the button sometimes does
* then hit the button, the button sometimes does not work. * not work.
* *
* When the combobox loses focus, * <p>
* and its text has changed, it generates an actionPerformed event as * When the combobox loses focus, and its text has changed, it generates an actionPerformed event
* though the user pressed &lt;Enter&gt; in the combo text field. This * as though the user pressed &lt;Enter&gt; in the combo text field. This has a bizarre effect if
* has a bizarre effect if you have added an actionPerformed listener * you have added an actionPerformed listener to the combobox and in your callback you adjust the
* to the combobox and in your callback you adjust the enablement state * enablement state of the button that you pressed (which caused the text field to lose focus) in
* of the button that you pressed (which caused the text field to lose * that you end up changing the button's internal state(by calling setEnabled(true or false)) in
* focus) in that you end up changing the button's internal state(by calling * the middle of the button press.
* setEnabled(true or false)) in the middle of the button press. *
* @param <E> the item type
*/ */
public class GhidraComboBox<E> extends JComboBox<E> implements GComponent { public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
private ArrayList<ActionListener> listeners = new ArrayList<>(); private ArrayList<ActionListener> listeners = new ArrayList<>();
@ -74,17 +76,16 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
/** /**
* Construct a new GhidraComboBox using the given model. * Construct a new GhidraComboBox using the given model.
* @see javax.swing.JComboBox#JComboBox(ComboBoxModel) * @param model the model
*/ */
public GhidraComboBox(ComboBoxModel<E> aModel) { public GhidraComboBox(ComboBoxModel<E> model) {
super(aModel); super(model);
init(); init();
} }
/** /**
* Construct a new GhidraComboBox and populate a default model * Construct a new GhidraComboBox and populate a default model with the given items.
* with the given items. * @param items the items
* @see javax.swing.JComboBox#JComboBox(Object[])
*/ */
public GhidraComboBox(E[] items) { public GhidraComboBox(E[] items) {
super(items); super(items);
@ -92,12 +93,11 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
} }
/** /**
* Construct a new GhidraComboBox and populate a default model with * Construct a new GhidraComboBox and populate a default model with the given items.
* the given Vector of items. * @param items the items
* @see javax.swing.JComboBox#JComboBox(Vector)
*/ */
public GhidraComboBox(Vector<E> items) { public GhidraComboBox(Collection<E> items) {
super(items); super(new Vector<>(items));
init(); init();
} }
@ -155,10 +155,10 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
} }
/** /**
* HACK ALERT: By default, the JComboBoxUI forwards the &lt;Enter&gt; key actions to the root pane * HACK ALERT: By default, the JComboBoxUI forwards the &lt;Enter&gt; key actions to the root
* of the JComboBox's container (which is used primarily by any installed 'default button'). * pane of the JComboBox's container (which is used primarily by any installed 'default
* The problem is that the forwarding does not happen always. In the case that the &lt;Enter&gt; * button'). The problem is that the forwarding does not happen always. In the case that the
* key will trigger a selection in the combo box, the action is NOT forwarded. * &lt;Enter&gt; key will trigger a selection in the combo box, the action is NOT forwarded.
* <p> * <p>
* By default Ghidra disables the forwarding altogether, since most users of * By default Ghidra disables the forwarding altogether, since most users of
* {@link GhidraComboBox} will add an action listener to handle &lt;Enter&gt; actions. * {@link GhidraComboBox} will add an action listener to handle &lt;Enter&gt; actions.
@ -211,14 +211,17 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
/** /**
* A fix for the following series of events: * A fix for the following series of events:
* -The user selects an item * <ol>
* -The user deletes the text * <li>The user selects an item</li>
* -setSelectedItem(Object) method is called with the same item * <li>The user deletes the text</li>
* <li>setSelectedItem(Object) method is called with the same item</li>
* </ol>
* *
* In that above series of steps, the text will still be empty, as the user deleted it *and* * In that above series of steps, the text will still be empty, as the user deleted it *and*
* the call to setSelectedItem(Object) had no effect because the base class assumed that the * the call to setSelectedItem(Object) had no effect because the base class assumed that the
* item is already selected. * item is already selected.
* *
* <p>
* This method exists to make sure, in that case, that the text of the field matches the * This method exists to make sure, in that case, that the text of the field matches the
* selected item. * selected item.
*/ */
@ -243,9 +246,6 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
} }
} }
/**
* Remove all entries in the drop down list
*/
public void clearModel() { public void clearModel() {
DefaultComboBoxModel<E> model = (DefaultComboBoxModel<E>) getModel(); DefaultComboBoxModel<E> model = (DefaultComboBoxModel<E>) getModel();
model.removeAllElements(); model.removeAllElements();
@ -325,15 +325,15 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
} }
/** /**
* Custom Document the valid user input on the fly. * Custom Document to perform matching of items as the user types
*/ */
public class InterceptedInputDocument extends DefaultStyledDocument { public class InterceptedInputDocument extends DefaultStyledDocument {
private boolean automated = false; private boolean automated = false;
/** /**
* Called before new user input is inserted into the entry text field. The super * Called before new user input is inserted into the entry text field. The super method is
* method is called if the input is accepted. * called if the input is accepted.
*/ */
@Override @Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {

View file

@ -0,0 +1,73 @@
/* ###
* 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 static org.junit.Assert.*;
import java.util.List;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class FindDialogTest {
@Test
public void testSetSelectedValueDoesNotTriggerMatch() {
FindDialogSearcher searcher = new DummySearcher();
FindDialog findDialog = new FindDialog("Title", searcher);
findDialog.setHistory(List.of("search1"));
String searchText = "search"; // a prefix of an existing history entry
findDialog.setSearchText(searchText);
assertEquals(searchText, AbstractGenericTest.runSwing(() -> findDialog.getSearchText()));
}
private class DummySearcher implements FindDialogSearcher {
@Override
public CursorPosition getCursorPosition() {
return new CursorPosition(0);
}
@Override
public void setCursorPosition(CursorPosition position) {
// stub
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
}
@Override
public CursorPosition getEnd() {
return new CursorPosition(1);
}
@Override
public void highlightSearchResults(SearchLocation location) {
// stub
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
return null;
}
}
}