Merge remote-tracking branch 'origin/GP-1-dragonmacher-escape-confirmation'

This commit is contained in:
Ryan Kurtz 2025-04-21 14:12:54 -04:00
commit a810c384f4
11 changed files with 227 additions and 64 deletions

View file

@ -65,7 +65,6 @@ public class CreateInternalStructureAction extends CompositeEditorTableAction {
createStructure();
}
requestTableFocus();
Swing.runLater(() -> {
provider.toFront();
provider.requestFocus();

View file

@ -32,7 +32,7 @@ public class AllHistoryAction extends ListingContextAction {
super("Show All History", owner);
this.tool = tool;
setMenuBarData(new MenuData(new String[] { ToolConstants.MENU_SEARCH, "Label History..." },
null, "Search"));
null, "search 1"));
setKeyBindingData(new KeyBindingData(KeyEvent.VK_H, 0));
addToWindowWhen(ListingActionContext.class);

View file

@ -43,6 +43,10 @@ public enum Combiner {
this.function = function;
}
public boolean isMerge() {
return this != REPLACE;
}
public String getName() {
return name;
}

View file

@ -249,8 +249,9 @@ public class MemoryMatchTableModel extends AddressBasedTableModel<MemoryMatch> {
byteString = HTMLUtilities.colorString(color, byteString);
}
b.append(byteString);
if (i == max)
if (i == max) {
break;
}
b.append(" ");
}

View file

@ -274,6 +274,10 @@ class MemorySearchControlPanel extends JPanel {
searchButton.setSelectedStateByClientData(combiner);
}
void setSearchCombiner(Combiner combiner) {
searchButton.setSelectedStateByClientData(combiner);
}
private void adjustLocationForCaretPosition(Point location) {
JTextField textField = searchInputField.getTextField();
Caret caret = textField.getCaret();
@ -460,5 +464,4 @@ class MemorySearchControlPanel extends JPanel {
Component getDefaultFocusComponent() {
return searchInputField;
}
}

View file

@ -149,6 +149,7 @@ public class MemorySearchPlugin extends Plugin implements MemorySearchService {
saveState.putBoolean(SHOW_OPTIONS_PANEL, showOptionsPanel);
saveState.putBoolean(SHOW_SCAN_PANEL, showOptionsPanel);
}
//==================================================================================================
// MemorySearchService methods
//==================================================================================================
@ -205,10 +206,9 @@ public class MemorySearchPlugin extends Plugin implements MemorySearchService {
Msg.showWarn(this, null, "Search Failed!", "No valid start address!");
return;
}
MemorySearcher searcher = new MemorySearcher(source, lastByteMatcher, addresses, 1);
MemoryMatch match = searcher.findOnce(start, forward, monitor);
Swing.runLater(() -> navigateToMatch(match));
}

View file

@ -31,6 +31,8 @@ import docking.action.ToggleDockingAction;
import docking.action.builder.ActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import docking.util.GGlassPaneMessage;
import docking.widgets.OptionDialog;
import docking.widgets.OptionDialogBuilder;
import generic.theme.GIcon;
import ghidra.app.context.NavigatableActionContext;
import ghidra.app.nav.Navigatable;
@ -38,6 +40,7 @@ import ghidra.app.nav.NavigatableRemovalListener;
import ghidra.app.util.HelpTopics;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.features.base.memsearch.bytesource.SearchRegion;
import ghidra.features.base.memsearch.combiner.Combiner;
import ghidra.features.base.memsearch.matcher.ByteMatcher;
import ghidra.features.base.memsearch.scan.Scanner;
import ghidra.features.base.memsearch.searcher.*;
@ -145,7 +148,6 @@ public class MemorySearchProvider extends ComponentProviderAdapter
navigatable.addNavigatableListener(this);
program.addCloseListener(this);
updateTitle();
}
public void setSearchInput(String input) {
@ -539,9 +541,43 @@ public class MemorySearchProvider extends ComponentProviderAdapter
public void actionPerformed(ActionContext context) {
super.actionPerformed(context);
updateSubTitle();
resultsPanel.itemDeleted();
}
});
}
@Override
public void closeComponent() {
doClose(false);
}
private void doClose(boolean force) {
if (force) {
super.closeComponent();
return;
}
if (!canClose()) {
return;
}
super.closeComponent();
}
private boolean canClose() {
boolean hasUserChanges = resultsPanel.hasUserChanges();
if (!hasUserChanges) {
return true;
}
String message = "Close dialog and lost custom search results?";
OptionDialogBuilder builder = new OptionDialogBuilder("Close Results Window?", message);
int choice = builder.addOption("Yes")
.addCancel()
.setDefaultButton("Yes")
.setMessageType(OptionDialog.QUESTION_MESSAGE)
.show(resultsPanel);
return choice == OptionDialog.OPTION_ONE;
}
@Override
@ -551,6 +587,7 @@ public class MemorySearchProvider extends ComponentProviderAdapter
}
private void dispose() {
if (glassPaneMessage != null) {
glassPaneMessage.hide();
glassPaneMessage = null;
@ -583,12 +620,12 @@ public class MemorySearchProvider extends ComponentProviderAdapter
@Override
public void navigatableRemoved(Navigatable nav) {
closeComponent();
doClose(true);
}
@Override
public void domainObjectClosed(DomainObject dobj) {
closeComponent();
doClose(true);
}
Navigatable getNavigatable() {
@ -638,12 +675,20 @@ public class MemorySearchProvider extends ComponentProviderAdapter
return resultsPanel.getTableModel().getModelData();
}
public MemorySearchResultsPanel getResultsPanel() {
return resultsPanel;
}
public void setSettings(SearchSettings settings) {
String converted = searchPanel.convertInput(model.getSettings(), settings);
model.setSettings(settings);
searchPanel.setSearchInput(converted);
}
public void setSearchCombiner(Combiner combiner) {
searchPanel.setSearchCombiner(combiner);
}
public boolean isSearchSelection() {
return model.isSearchSelectionOnly();
}

View file

@ -41,7 +41,7 @@ import ghidra.util.task.*;
* in a table. This panel also includes most of the search logic as it has direct access to the
* table for showing the results.
*/
class MemorySearchResultsPanel extends JPanel {
public class MemorySearchResultsPanel extends JPanel {
private GhidraThreadedTablePanel<MemoryMatch> threadedTablePanel;
private GhidraTableFilterPanel<MemoryMatch> tableFilterPanel;
private GhidraTable table;
@ -49,6 +49,9 @@ class MemorySearchResultsPanel extends JPanel {
private MemorySearchProvider provider;
private SearchMarkers markers;
private boolean hasDeleted;
private boolean hasCombined;
MemorySearchResultsPanel(MemorySearchProvider provider, SearchMarkers markers) {
super(new BorderLayout());
this.provider = provider;
@ -73,6 +76,14 @@ class MemorySearchResultsPanel extends JPanel {
markers.loadMarkers(provider.getTitle(), tableModel.getModelData());
}
void itemDeleted() {
hasDeleted = true;
}
boolean hasUserChanges() {
return hasDeleted || hasCombined;
}
void providerActivated() {
markers.makeActiveMarkerSet();
}
@ -109,7 +120,13 @@ class MemorySearchResultsPanel extends JPanel {
}
private MemoryMatchTableLoader createLoader(MemorySearcher searcher, Combiner combiner) {
if (hasResults()) {
if (!hasResults()) {
hasDeleted = false;
return new NewSearchTableLoader(searcher);
}
// We have existing results. Will they be merged?
if (combiner.isMerge()) {
// If we have existing results, the combiner determines how the new search results get
// combined with the existing results.
@ -118,11 +135,14 @@ class MemorySearchResultsPanel extends JPanel {
// and only the new results are kept. In this case, it is preferred to use the same
// loader as if doing an initial search because you get incremental loading and also
// don't need to copy the existing results to feed to a combiner.
if (combiner != Combiner.REPLACE) {
List<MemoryMatch> previousResults = tableModel.getModelData();
return new CombinedMatchTableLoader(searcher, previousResults, combiner);
}
hasCombined = true;
List<MemoryMatch> previousResults = tableModel.getModelData();
return new CombinedMatchTableLoader(searcher, previousResults, combiner);
}
// We have results, but we are going to replace them. A new load of data means any previous
// manual deletes are now irrelevant
hasDeleted = false;
return new NewSearchTableLoader(searcher);
}
@ -146,7 +166,7 @@ class MemorySearchResultsPanel extends JPanel {
}
}
GhidraTable getTable() {
public GhidraTable getTable() {
return table;
}

View file

@ -24,12 +24,17 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import docking.action.DockingActionIf;
import docking.widgets.OptionDialog;
import docking.widgets.fieldpanel.support.Highlight;
import docking.widgets.table.GTable;
import ghidra.GhidraOptions;
import ghidra.app.services.MarkerSet;
import ghidra.app.util.viewer.field.BytesFieldFactory;
import ghidra.features.base.memsearch.bytesource.ProgramSearchRegion;
import ghidra.features.base.memsearch.combiner.Combiner;
import ghidra.features.base.memsearch.format.SearchFormat;
import ghidra.features.base.memsearch.gui.MemorySearchResultsPanel;
import ghidra.framework.options.Options;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.address.Address;
@ -259,15 +264,13 @@ public class MemSearchHexTest extends AbstractMemSearchTest {
@Test
public void testHexSearchAll2() throws Exception {
// enter search string for multiple byte match
// enter search string for multiple byte match
setInput("ff 15");
performSearchAll();
waitForSearch(5);
List<Address> addrs = addrs(0x01002d1f, 0x01002d41, 0x01002d4a, 0x01002d5e, 0x010029bd);
checkMarkerSet(addrs);
}
@ -552,4 +555,113 @@ public class MemSearchHexTest extends AbstractMemSearchTest {
assertEquals(addr(0x01002d0b), currentAddress());
}
@Test
public void testPromptToClose_NoChanges() {
search("ff 15", 5);
triggerEscape(searchProvider.getComponent());
assertProviderClosed();
}
@Test
public void testPromptToClose_DeletedRows() {
search("ff 15", 5);
deleteRow(0);
triggerEscape(searchProvider.getComponent());
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
pressButtonByText(dialog, "Yes");
assertProviderClosed();
}
@Test
public void testPromptToClose_DeletedRows_Cancel() {
search("ff 15", 5);
deleteRow(0);
triggerEscape(searchProvider.getComponent());
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
pressButtonByText(dialog, "No");
assertProviderVisible();
}
@Test
public void testPromptToClose_MergedData() {
//
// Search then perform a new search that will be combined with the initial search results.
// This new merged search will trigger the requirement to prompt the user before closing,
// since the new set of data is non-trivial to create.
//
search("ff 15", 5);
// perform a new search and use the existing results
runSwing(() -> searchProvider.setSearchCombiner(Combiner.UNION));
search("8b f?", 9);
triggerEscape(searchProvider.getComponent());
OptionDialog confirmDialog = waitForDialogComponent(OptionDialog.class);
pressButtonByText(confirmDialog, "Yes");
assertProviderClosed();
}
@Test
public void testPromptToClose_DeletedRows_NewSearch() {
//
// Search. Delete a row. This would trigger a prompt when closing the dialog. Perform a
// new search. This new non-merged search will clear the requirement to prompt the user
// before closing.
//
search("ff 15", 5);
deleteRow(0);
search("8b f?", 4);
triggerEscape(searchProvider.getComponent());
assertProviderClosed();
}
private void assertProviderVisible() {
assertTrue(runSwing(() -> searchProvider.isVisible()));
}
private void assertProviderClosed() {
assertFalse(runSwing(() -> searchProvider.isVisible()));
}
private void search(String input, int expectedMatchCount) {
setInput(input);
performSearchAll();
waitForSearch(expectedMatchCount);
}
private void deleteRow(int row) {
int resultCount = getResultCount();
runSwing(() -> {
MemorySearchResultsPanel panel = searchProvider.getResultsPanel();
GTable table = panel.getTable();
table.selectRow(row);
});
DockingActionIf removeAction = getAction(memorySearchPlugin, "Remove Items");
performAction(removeAction);
assertEquals(resultCount - 1, getResultCount());
}
private int getResultCount() {
return runSwing(() -> searchProvider.getSearchResults().size());
}
}

View file

@ -22,11 +22,9 @@ import java.awt.Window;
import org.junit.*;
import docking.action.DockingActionIf;
import ghidra.app.events.ProgramSelectionPluginEvent;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.marker.MarkerManagerPlugin;
import ghidra.app.plugin.core.programtree.ProgramTreePlugin;
import ghidra.app.services.ProgramManager;
import ghidra.features.base.memsearch.gui.MemorySearchPlugin;
import ghidra.features.base.memsearch.gui.MemorySearchProvider;
import ghidra.features.base.memsearch.mnemonic.MnemonicSearchPlugin;
@ -35,7 +33,6 @@ import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramSelection;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.Swing;
@ -45,9 +42,9 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
private PluginTool tool;
private ProgramDB program;
private MnemonicSearchPlugin plugin;
private DockingActionIf searchMnemonicOperandsNoConstAction;
private DockingActionIf searchMnemonicNoOperandsNoConstAction;
private DockingActionIf searchMnemonicOperandsConstAction;
private DockingActionIf includeOperandsExcludeConstAction;
private DockingActionIf excludeOperandsAction;
private DockingActionIf includeOperandsAction;
private CodeBrowserPlugin cb;
private MemorySearchProvider searchProvider;
@ -63,16 +60,12 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
plugin = env.getPlugin(MnemonicSearchPlugin.class);
cb = env.getPlugin(CodeBrowserPlugin.class);
program = (ProgramDB) buildProgram();
env.showTool(program);
ProgramManager pm = tool.getService(ProgramManager.class);
pm.openProgram(program.getDomainFile());
searchMnemonicOperandsNoConstAction =
excludeOperandsAction = getAction(plugin, "Exclude Operands");
includeOperandsAction = getAction(plugin, "Include Operands");
includeOperandsExcludeConstAction =
getAction(plugin, "Include Operands (except constants)");
searchMnemonicNoOperandsNoConstAction = getAction(plugin, "Exclude Operands");
searchMnemonicOperandsConstAction = getAction(plugin, "Include Operands");
env.showTool();
}
private Program buildProgram() throws Exception {
@ -93,29 +86,19 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
@Test
public void testSearchMnemonicOperandsNoConst() {
ProgramSelection sel = new ProgramSelection(addr(0x01004062), addr(0x0100406a));
tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", sel, program));
performAction(searchMnemonicOperandsNoConstAction, cb.getProvider(), true);
makeSelection(tool, program, addr(0x01004062), addr(0x0100406a));
performAction(includeOperandsExcludeConstAction, cb.getProvider(), true);
searchProvider = waitForComponentProvider(MemorySearchProvider.class);
assertNotNull(searchProvider);
assertEquals(
"01010101 10001011 11101100 10000001 11101100 ........ ........ ........ ........",
getInput());
}
@Test
public void testSearchMnemonicNoOperandsNoConst() {
ProgramSelection sel = new ProgramSelection(addr(0x01004062), addr(0x0100406a));
tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", sel, program));
performAction(searchMnemonicNoOperandsNoConstAction, cb.getProvider(), true);
makeSelection(tool, program, addr(0x01004062), addr(0x0100406a));
performAction(excludeOperandsAction, cb.getProvider(), true);
searchProvider = waitForComponentProvider(MemorySearchProvider.class);
assertNotNull(searchProvider);
assertEquals(
"01010... 10001011 11...... 10000001 11101... ........ ........ ........ ........",
getInput());
@ -124,16 +107,10 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
@Test
public void testSearchMnemonicOperandsConst() {
ProgramSelection sel = new ProgramSelection(addr(0x01004062), addr(0x0100406a));
tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", sel, program));
performAction(searchMnemonicOperandsConstAction, cb.getProvider(), true);
performAction(searchMnemonicOperandsConstAction, cb.getProvider(), true);
makeSelection(tool, program, addr(0x01004062), addr(0x0100406a));
performAction(includeOperandsAction, cb.getProvider(), true);
searchProvider = waitForComponentProvider(MemorySearchProvider.class);
assertNotNull(searchProvider);
assertEquals(
"01010101 10001011 11101100 10000001 11101100 00000100 00000001 00000000 00000000",
getInput());
@ -142,7 +119,6 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
/**
* Tests that when multiple regions are selected, the user is notified via
* pop-up that this is not acceptable.
*
*/
@Test
public void testMultipleSelection() {
@ -158,7 +134,7 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
makeSelection(tool, program, addrSet);
// Now invoke the menu option we want to test.
performAction(searchMnemonicOperandsConstAction, cb.getProvider(), false);
performAction(includeOperandsAction, cb.getProvider(), false);
// Here's the main assert: If the code recognizes that we have multiple selection, the
// MemSearchDialog will NOT be displayed (an error message pops up instead). So verify that
@ -172,7 +148,7 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes
return program.getMinAddress().getNewAddress(offset);
}
protected String getInput() {
private String getInput() {
return Swing.runNow(() -> searchProvider.getSearchInput());
}
}

View file

@ -76,8 +76,11 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
String message = getMessage(fileCount, files);
OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message);
builder.addOption("OK").addCancel().setMessageType(OptionDialog.QUESTION_MESSAGE);
return builder.show(parent) != OptionDialog.CANCEL_OPTION;
int choice = builder.addOption("OK")
.addCancel()
.setMessageType(OptionDialog.QUESTION_MESSAGE)
.show(parent);
return choice != OptionDialog.CANCEL_OPTION;
}
private String getMessage(int fileCount, Set<DomainFile> selectedFiles) {