Merge remote-tracking branch

'origin/GP-3858-dragonmacher-console-find--SQUASHED'
(Closes #2567, #7136)
This commit is contained in:
Ryan Kurtz 2025-01-22 09:04:04 -05:00
commit 4db0ccc8ec
24 changed files with 1525 additions and 505 deletions

View file

@ -24,7 +24,7 @@ color.fg.dialog.equates.equate = color.palette.blue
color.fg.dialog.equates.suggestion = color.palette.hint
color.fg.consoletextpane = color.fg
color.fg.error.consoletextpane = color.fg.error
color.fg.consoletextpane.error = color.fg.error
color.fg.infopanel.version = color.fg
@ -34,8 +34,9 @@ color.fg.interpreterconsole.error = color.fg.error
color.bg.markerservice = color.bg
color.bg.search.highlight = color.bg.highlight
color.bg.search.highlight.current.line = color.palette.yellow
color.bg.search.highlight = color.bg.find.highlight
color.bg.search.highlight.current.line = color.bg.find.highlight.active
color.fg.analysis.options.prototype = color.palette.crimson
color.fg.analysis.options.prototype.selected = color.palette.lightcoral

View file

@ -20,11 +20,13 @@ import java.awt.event.*;
import java.io.PrintWriter;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.*;
import docking.*;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.app.services.*;
@ -53,13 +55,19 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
private ConsoleTextPane textPane;
private JScrollPane scroller;
private JComponent component;
private boolean scrollLock = false;
private DockingAction clearAction;
private ToggleDockingAction scrollAction;
private Address currentAddress;
private PrintWriter stderr;
private PrintWriter stdin;
private boolean scrollLock = false;
private DockingAction clearAction;
private ToggleDockingAction scrollAction;
private Program currentProgram;
private Address currentAddress;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public ConsoleComponentProvider(PluginTool tool, String owner) {
super(tool, "Console", owner);
@ -97,6 +105,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
textPane.dispose();
stderr.close();
stdin.close();
if (findDialog != null) {
findDialog.close();
}
}
private void createOptions() {
@ -117,70 +129,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
textPane.setName(textPaneName);
textPane.getAccessibleContext().setAccessibleName(textPaneName);
textPane.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (currentProgram == null) {
return;
}
Point hoverPoint = e.getPoint();
ConsoleWord word = getWordSeparatedByWhitespace(hoverPoint);
if (word == null) {
textPane.setCursor(Cursor.getDefaultCursor());
return;
}
Address addr = currentProgram.getAddressFactory().getAddress(word.word);
if (addr != null || isSymbol(word.word)) {
textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word);
if (addr != null || isSymbol(trimmedWord.word)) {
textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
}
});
textPane.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getClickCount() != 2) {
return;
}
if (currentProgram == null) {
return;
}
GoToService gotoService = tool.getService(GoToService.class);
if (gotoService == null) {
return;
}
Point clickPoint = e.getPoint();
ConsoleWord word = getWordSeparatedByWhitespace(clickPoint);
if (word == null) {
return;
}
Address addr = currentProgram.getAddressFactory().getAddress(word.word);
if (addr != null || isSymbol(word.word)) {
goTo(word);
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word);
if (addr == null && !isSymbol(trimmedWord.word)) {
return;
}
goTo(trimmedWord);
}
});
textPane.addMouseMotionListener(new CursorUpdateMouseMotionListener());
textPane.addMouseListener(new GoToMouseListener());
scroller = new JScrollPane(textPane);
scroller.setPreferredSize(new Dimension(200, 100));
@ -191,74 +141,12 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
tool.addComponentProvider(this, true);
}
private void goTo(ConsoleWord word) {
GoToService gotoService = tool.getService(GoToService.class);
if (gotoService == null) {
return;
}
// NOTE: must be case sensitive otherwise the service will report that it has
// processed the request even if there are no matches
boolean found =
gotoService.goToQuery(currentAddress, new QueryData(word.word, true), null, null);
if (found) {
select(word);
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
found = gotoService.goToQuery(currentAddress, new QueryData(trimmedWord.word, true), null,
null);
if (found) {
select(trimmedWord);
}
}
private ConsoleWord getWordSeparatedByWhitespace(Point p) {
int pos = textPane.viewToModel2D(p);
Document doc = textPane.getDocument();
int startIndex = pos;
int endIndex = pos;
try {
for (; startIndex > 0; --startIndex) {
char c = doc.getText(startIndex, 1).charAt(0);
if (Character.isWhitespace(c)) {
break;
}
}
for (; endIndex < doc.getLength() - 1; ++endIndex) {
char c = doc.getText(endIndex, 1).charAt(0);
if (Character.isWhitespace(c)) {
break;
}
}
String text = doc.getText(startIndex + 1, endIndex - startIndex);
if (text == null || text.trim().length() == 0) {
return null;
}
return new ConsoleWord(text.trim(), startIndex + 1, endIndex);
}
catch (BadLocationException ble) {
return null;
}
}
private boolean isSymbol(String word) {
SymbolTable symbolTable = currentProgram.getSymbolTable();
SymbolIterator symbolIterator = symbolTable.getSymbols(word);
return symbolIterator.hasNext();
}
protected void select(ConsoleWord word) {
try {
textPane.select(word.startPosition, word.endPosition);
}
catch (Exception e) {
// we are too lazy to verify our data before calling select--bleh
}
}
private void createActions() {
clearAction = new DockingAction("Clear Console", getOwner()) {
@ -268,9 +156,7 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
}
};
clearAction.setDescription("Clear Console");
// ACTIONS - auto generated
clearAction.setToolBarData(new ToolBarData(new GIcon("icon.plugin.console.clear"), null));
clearAction.setEnabled(true);
scrollAction = new ToggleDockingAction("Scroll Lock", getOwner()) {
@ -282,14 +168,33 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
scrollAction.setDescription("Scroll Lock");
scrollAction.setToolBarData(
new ToolBarData(new GIcon("icon.plugin.console.scroll.lock"), null));
scrollAction.setEnabled(true);
scrollAction.setSelected(scrollLock);
//@formatter:off
new ActionBuilder("Find", getOwner())
.keyBinding("Ctrl F")
.sharedKeyBinding()
.popupMenuPath("Find...")
.onAction(c -> {
showFindDialog();
})
.buildAndInstallLocal(this)
;
//@formatter:on
addLocalAction(scrollAction);
addLocalAction(clearAction);
}
private void showFindDialog() {
if (findDialog == null) {
searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Find", searcher);
}
getTool().showDialog(findDialog);
}
@Override
public void addMessage(String originator, String message) {
checkVisible();
@ -304,26 +209,6 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
@Override
public void addException(String originator, Exception e) {
try {
e.printStackTrace(stderr);
}
catch (Exception e1) {
//
// sometimes an exception will occur while printing
// the stack trace on an exception.
// if that happens catch it and manually print
// some information about it.
// see org.jruby.exceptions.RaiseException
//
stderr.println("Unexpected Exception: " + e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
stderr.println("\t" + stackTraceElement.toString());
}
stderr.println("Unexpected Exception: " + e1.getMessage());
for (StackTraceElement stackTraceElement : e1.getStackTrace()) {
stderr.println("\t" + stackTraceElement.toString());
}
}
Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
}
@ -331,6 +216,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
public void clearMessages() {
checkVisible();
textPane.setText("");
if (searcher != null) {
searcher.clearHighlights();
}
}
@Override
@ -383,22 +272,21 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
return textPane.getDocument().getLength();
}
////////////////////////////////////////////////////////////////////
private void checkVisible() {
if (!isVisible()) {
tool.showComponentProvider(this, true);
}
}
/**
* @see docking.ComponentProvider#getComponent()
*/
@Override
public JComponent getComponent() {
return component;
}
ConsoleTextPane getTextPane() {
return textPane;
}
public void setCurrentProgram(Program program) {
currentProgram = program;
}
@ -407,4 +295,136 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
currentAddress = address;
}
private static ConsoleWord getWordSeparatedByWhitespace(JTextComponent textComponent, Point p) {
int pos = textComponent.viewToModel2D(p);
Document doc = textComponent.getDocument();
int startIndex = pos;
int endIndex = pos;
try {
for (; startIndex > 0; --startIndex) {
char c = doc.getText(startIndex, 1).charAt(0);
if (Character.isWhitespace(c)) {
break;
}
}
for (; endIndex < doc.getLength() - 1; ++endIndex) {
char c = doc.getText(endIndex, 1).charAt(0);
if (Character.isWhitespace(c)) {
break;
}
}
String text = doc.getText(startIndex + 1, endIndex - startIndex);
if (text == null || text.trim().length() == 0) {
return null;
}
return new ConsoleWord(text.trim(), startIndex + 1, endIndex);
}
catch (BadLocationException ble) {
return null;
}
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class GoToMouseListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.getClickCount() != 2) {
return;
}
if (currentProgram == null) {
return;
}
GoToService gotoService = tool.getService(GoToService.class);
if (gotoService == null) {
return;
}
Point clickPoint = e.getPoint();
ConsoleWord word = getWordSeparatedByWhitespace(textPane, clickPoint);
if (word == null) {
return;
}
Address addr = currentProgram.getAddressFactory().getAddress(word.word);
if (addr != null || isSymbol(word.word)) {
goTo(word);
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word);
if (addr == null && !isSymbol(trimmedWord.word)) {
return;
}
goTo(trimmedWord);
}
private void goTo(ConsoleWord word) {
GoToService gotoService = tool.getService(GoToService.class);
if (gotoService == null) {
return;
}
// NOTE: must be case sensitive otherwise the service will report that it has
// processed the request even if there are no matches
boolean found =
gotoService.goToQuery(currentAddress, new QueryData(word.word, true), null, null);
if (found) {
select(word);
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
found =
gotoService.goToQuery(currentAddress, new QueryData(trimmedWord.word, true), null,
null);
if (found) {
select(trimmedWord);
}
}
private void select(ConsoleWord word) {
try {
textPane.select(word.startPosition, word.endPosition);
}
catch (Exception e) {
// we are too lazy to verify our data before calling select--bleh
}
}
}
private class CursorUpdateMouseMotionListener extends MouseMotionAdapter {
@Override
public void mouseMoved(MouseEvent e) {
if (currentProgram == null) {
return;
}
Point hoverPoint = e.getPoint();
ConsoleWord word = getWordSeparatedByWhitespace(textPane, hoverPoint);
if (word == null) {
textPane.setCursor(Cursor.getDefaultCursor());
return;
}
Address addr = currentProgram.getAddressFactory().getAddress(word.word);
if (addr != null || isSymbol(word.word)) {
textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters();
addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word);
if (addr != null || isSymbol(trimmedWord.word)) {
textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
}
}
}

View file

@ -45,7 +45,6 @@ class FlowArrowPanel extends JPanel {
private Point pendingMouseClickPoint;
FlowArrowPanel(FlowArrowPlugin p) {
super();
this.plugin = p;
setMinimumSize(new Dimension(0, 0));
setPreferredSize(new Dimension(32, 1));
@ -164,7 +163,6 @@ class FlowArrowPanel extends JPanel {
ScrollingCallback callback = new ScrollingCallback(start, end);
Animator animator = AnimationUtils.executeSwingAnimationCallback(callback);
callback.setAnimator(animator);
}
private void processSingleClick(Point point) {
@ -322,7 +320,9 @@ class FlowArrowPanel extends JPanel {
if (current.equals(end)) {
// we are done!
if (animator != null) {
animator.stop();
}
return;
}

View file

@ -19,13 +19,13 @@ import java.io.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.*;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import docking.widgets.OptionDialog;
import docking.action.builder.ActionBuilder;
import docking.widgets.*;
import generic.theme.GIcon;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.util.HelpLocation;
@ -39,6 +39,9 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
private InterpreterConnection interpreter;
private List<Callback> firstActivationCallbacks;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public InterpreterComponentProvider(InterpreterPanelPlugin plugin,
InterpreterConnection interpreter, boolean visible) {
super(plugin.getTool(), interpreter.getTitle(), interpreter.getTitle());
@ -72,8 +75,28 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
clearAction.setDescription("Clear Interpreter");
clearAction.setToolBarData(new ToolBarData(Icons.CLEAR_ICON, null));
clearAction.setEnabled(true);
addLocalAction(clearAction);
//@formatter:off
new ActionBuilder("Find", getOwner())
.keyBinding("Ctrl F")
.sharedKeyBinding()
.popupMenuPath("Find...")
.onAction(c -> {
showFindDialog();
})
.buildAndInstallLocal(this)
;
//@formatter:on
}
private void showFindDialog() {
if (findDialog == null) {
JTextPane textPane = panel.getOutputTextPane();
searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Find", searcher);
}
getTool().showDialog(findDialog);
}
@Override
@ -128,6 +151,10 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
@Override
public void clear() {
panel.clear();
if (searcher != null) {
searcher.clearHighlights();
}
}
@Override

View file

@ -77,7 +77,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
private SimpleAttributeSet STDIN_SET;
private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB;
private boolean highlightCompletion = false;
private int completionInsertionPosition;
private boolean caretGuard = true;
@ -298,12 +297,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
completionWindowTrigger =
options.getEnum(COMPLETION_WINDOW_TRIGGER_LABEL, CompletionWindowTrigger.TAB);
// TODO
// highlightCompletion =
// options.getBoolean(HIGHLIGHT_COMPLETION_OPTION_LABEL, DEFAULT_HIGHLIGHT_COMPLETION);
// options.setDescription(HIGHLIGHT_COMPLETION_OPTION_LABEL, HIGHLIGHT_COMPLETION_DESCRIPTION);
// options.addOptionsChangeListener(this);
options.addOptionsChangeListener(this);
}
@ -317,10 +310,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
else if (optionName.equals(COMPLETION_WINDOW_TRIGGER_LABEL)) {
completionWindowTrigger = (CompletionWindowTrigger) newValue;
}
// TODO
// else if (optionName.equals(HIGHLIGHT_COMPLETION_OPTION_LABEL)) {
// highlightCompletion = ((Boolean) newValue).booleanValue();
// }
}
@Override
@ -480,6 +469,10 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
stdin.resetStream();
}
public JTextPane getOutputTextPane() {
return outputTextPane;
}
public String getOutputText() {
return outputTextPane.getText();
}
@ -535,16 +528,8 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
text.substring(0, insertedTextStart) + insertion + text.substring(position);
setInputTextPaneText(inputText);
/* Select what we inserted so that the user can easily
* get rid of what they did (in case of a mistake). */
if (highlightCompletion) {
inputTextPane.setSelectionStart(insertedTextStart);
inputTextPane.moveCaretPosition(insertedTextEnd);
}
else {
/* Then put the caret right after what we inserted. */
inputTextPane.setCaretPosition(insertedTextEnd);
}
updateCompletionList();
}

View file

@ -560,7 +560,7 @@ public class AddEditDialog extends ReusableDialogComponentProvider {
}
};
// the number of columns determines the default width of the add/edit label dialog
labelNameChoices.setColumnCount(20);
labelNameChoices.setColumns(20);
labelNameChoices.setName("label.name.choices");
GhidraComboBox<NamespaceWrapper> comboBox = new GhidraComboBox<>();
comboBox.setEnterKeyForwarding(true);

View file

@ -32,8 +32,6 @@ import ghidra.util.task.SwingUpdateManager;
/**
* A generic text pane that is used as a console to which text can be written.
*
* There is not test for this class, but it is indirectly tested by FrontEndGuiTest.
*/
public class ConsoleTextPane extends JTextPane implements OptionsChangeListener {
@ -211,7 +209,7 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener
outputAttributes = new GAttributes(font, new GColor("color.fg.consoletextpane"));
outputAttributes.addAttribute(CUSTOM_ATTRIBUTE_KEY, OUTPUT_ATTRIBUTE_VALUE);
errorAttributes = new GAttributes(font, new GColor("color.fg.error.consoletextpane"));
errorAttributes = new GAttributes(font, new GColor("color.fg.consoletextpane.error"));
errorAttributes.addAttribute(CUSTOM_ATTRIBUTE_KEY, ERROR_ATTRIBUTE_VALUE);
}

View file

@ -0,0 +1,482 @@
/* ###
* 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 ghidra.app.plugin.core.console;
import static org.junit.Assert.*;
import java.awt.Color;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.*;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
import javax.swing.text.Highlighter.Highlight;
import org.apache.logging.log4j.Level;
import org.junit.*;
import docking.action.DockingActionIf;
import docking.util.AnimationUtils;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.jar.ResourceFile;
import generic.theme.GColor;
import ghidra.app.script.GhidraScript;
import ghidra.framework.Application;
import ghidra.framework.main.ConsoleTextPane;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramDB;
import ghidra.test.*;
public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
private TestEnv env;
private ProgramDB program;
private PluginTool tool;
private ConsoleComponentProvider provider;
private ConsoleTextPane textPane;
private FindDialog findDialog;
private ConsolePlugin plugin;
@Before
public void setUp() throws Exception {
// turn off debug and info log statements that make the console noisy
setLogLevel(GhidraScript.class, Level.ERROR);
setLogLevel(ScriptTaskListener.class, Level.ERROR);
env = new TestEnv();
ToyProgramBuilder builder = new ToyProgramBuilder("sample", true);
program = builder.getProgram();
tool = env.launchDefaultTool(program);
plugin = env.getPlugin(ConsolePlugin.class);
provider = (ConsoleComponentProvider) tool.getComponentProvider("Console");
textPane = provider.getTextPane();
ResourceFile resourceFile =
Application.getModuleFile("Base", "ghidra_scripts/HelloWorldScript.java");
File scriptFile = resourceFile.getFile(true);
env.runScript(scriptFile);
AnimationUtils.setAnimationEnabled(false);
placeCursorAtBeginning();
findDialog = showFindDialog();
String searchText = "Hello";
find(searchText);
}
@After
public void tearDown() {
close(findDialog);
env.dispose();
}
@Test
public void testFindHighlights() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
verfyHighlightColor(matches);
close(findDialog);
verifyDefaultBackgroundColorForAllText();
}
@Test
public void testFindHighlights_ChangeSearchText() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
verfyHighlightColor(matches);
// Change the search text after the first search and make sure the new text is found and
// highlighted correctly.
String newSearchText = "java";
runSwing(() -> findDialog.setSearchText(newSearchText));
pressButtonByText(findDialog, "Next");
matches = getMatches();
assertEquals(3, matches.size());
verfyHighlightColor(matches);
close(findDialog);
verifyDefaultBackgroundColorForAllText();
}
@Test
public void testFindHighlights_ChangeDocumentText() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
verfyHighlightColor(matches);
runSwing(() -> textPane.setText("This is some\nnew text."));
verifyDefaultBackgroundColorForAllText();
assertSearchModelHasStaleSearchResults();
}
@Test
public void testMovingCursorUpdatesActiveHighlight() {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
TestTextMatch third = matches.get(2);
TestTextMatch last = matches.get(3);
placeCursonInMatch(second);
assertActiveHighlight(second);
placeCursonInMatch(third);
assertActiveHighlight(third);
placeCursonInMatch(first);
assertActiveHighlight(first);
placeCursonInMatch(last);
assertActiveHighlight(last);
}
@Test
public void testFindNext_ChangeDocumentText() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
assertCursorInMatch(first);
assertActiveHighlight(first);
next();
assertCursorInMatch(second);
assertActiveHighlight(second);
// Append text to the end of the document. This will cause the matches to be recalculated.
// The caret will remain on the current match.
appendText(" Hello, this is some\nnew text. Hello");
assertSearchModelHasStaleSearchResults();
// Pressing next will perform the search again. The caret is still at the position of the
// second match. That match will be found and highlighted again. (This will make the search
// appear as though the Next button did not move to the next match. Not sure if this is
// worth worrying about.)
next();
matches = getMatches();
assertEquals(6, matches.size()); // 4 old matches plus 2 new matches
second = matches.get(1);
assertCursorInMatch(second);
assertActiveHighlight(second);
next(); // third
next(); // fourth
next(); // fifth
next(); // sixth
TestTextMatch last = matches.get(5); // search wrapped
assertCursorInMatch(last);
assertActiveHighlight(last);
close(findDialog);
}
@Test
public void testFindNext() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
TestTextMatch third = matches.get(2);
TestTextMatch last = matches.get(3);
assertCursorInMatch(first);
assertActiveHighlight(first);
placeCursonInMatch(second);
assertActiveHighlight(second);
next();
assertCursorInMatch(third);
assertActiveHighlight(third);
next();
assertCursorInMatch(last);
assertActiveHighlight(last);
next();
assertCursorInMatch(first);
assertActiveHighlight(first);
close(findDialog);
}
@Test
public void testFindNext_MoveCaret() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch third = matches.get(2);
TestTextMatch last = matches.get(3);
assertCursorInMatch(first);
assertActiveHighlight(first);
placeCursonInMatch(third);
assertActiveHighlight(third);
next();
assertCursorInMatch(last);
assertActiveHighlight(last);
close(findDialog);
}
@Test
public void testFindPrevious() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
TestTextMatch third = matches.get(2);
TestTextMatch last = matches.get(3);
assertCursorInMatch(first);
assertActiveHighlight(first);
previous();
assertCursorInMatch(last);
assertActiveHighlight(last);
previous();
assertCursorInMatch(third);
assertActiveHighlight(third);
previous();
assertCursorInMatch(second);
assertActiveHighlight(second);
previous();
assertCursorInMatch(first);
assertActiveHighlight(first);
close(findDialog);
}
@Test
public void testFindPrevious_MoveCaret() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
TestTextMatch third = matches.get(2);
assertCursorInMatch(first);
assertActiveHighlight(first);
placeCursonInMatch(third);
assertActiveHighlight(third);
previous();
assertCursorInMatch(second);
assertActiveHighlight(second);
close(findDialog);
}
@Test
public void testClear() throws Exception {
List<TestTextMatch> matches = getMatches();
assertEquals(4, matches.size());
verfyHighlightColor(matches);
clear();
assertSearchModelHasNoSearchResults();
}
private void appendText(String text) {
runSwing(() -> {
Document document = textPane.getDocument();
int length = document.getLength();
try {
document.insertString(length, text, null);
}
catch (BadLocationException e) {
failWithException("Failed to append text", e);
}
});
waitForSwing(); // wait for the buffered response
}
private void clear() {
DockingActionIf action = getAction(plugin, "Clear Console");
performAction(action);
}
private void next() {
pressButtonByText(findDialog, "Next");
waitForSwing();
}
private void previous() {
pressButtonByText(findDialog, "Previous");
waitForSwing();
}
private void assertSearchModelHasNoSearchResults() {
TextComponentSearcher searcher =
(TextComponentSearcher) findDialog.getSearcher();
assertFalse(searcher.hasSearchResults());
}
private void assertSearchModelHasStaleSearchResults() {
TextComponentSearcher searcher =
(TextComponentSearcher) findDialog.getSearcher();
assertTrue(searcher.isStale());
}
private void assertCursorInMatch(TestTextMatch match) {
int pos = runSwing(() -> textPane.getCaretPosition());
waitForSwing();
assertTrue("Caret position %s not in match %s".formatted(pos, match),
match.start <= pos && pos <= match.end);
}
private void assertActiveHighlight(TestTextMatch match) {
GColor expectedHlColor = new GColor("color.bg.find.highlight.active");
assertActiveHighlight(match, expectedHlColor);
}
private void assertActiveHighlight(TestTextMatch match, Color expectedHlColor) {
Highlight matchHighlight = runSwing(() -> {
Highlighter highlighter = textPane.getHighlighter();
Highlight[] highlights = highlighter.getHighlights();
for (Highlight hl : highlights) {
int start = hl.getStartOffset();
int end = hl.getEndOffset();
if (start == match.start && end == match.end) {
return hl;
}
}
return null;
});
assertNotNull(matchHighlight);
DefaultHighlightPainter painter = (DefaultHighlightPainter) matchHighlight.getPainter();
Color actualHlColor = painter.getColor();
assertEquals(expectedHlColor, actualHlColor);
}
private void placeCursorAtBeginning() {
runSwing(() -> textPane.setCaretPosition(0));
waitForSwing();
}
private void placeCursonInMatch(TestTextMatch match) {
int pos = match.start;
runSwing(() -> textPane.setCaretPosition(pos));
waitForSwing();
}
private void verfyHighlightColor(List<TestTextMatch> matches)
throws Exception {
GColor nonActiveHlColor = new GColor("color.bg.find.highlight");
GColor activeHlColor = new GColor("color.bg.find.highlight.active");
int caret = textPane.getCaretPosition();
for (TestTextMatch match : matches) {
Color expectedColor = nonActiveHlColor;
if (match.contains(caret)) {
expectedColor = activeHlColor;
}
assertActiveHighlight(match, expectedColor);
}
}
private void verifyDefaultBackgroundColorForAllText() throws Exception {
StyledDocument styledDocument = textPane.getStyledDocument();
verifyDefaultBackgroundColorForAllText(styledDocument);
}
private void verifyDefaultBackgroundColorForAllText(StyledDocument document) throws Exception {
String text = document.getText(0, document.getLength());
for (int i = 0; i < text.length(); i++) {
AttributeSet charAttrs = document.getCharacterElement(i).getAttributes();
Color actualBgColor = StyleConstants.getBackground(charAttrs);
assertNotEquals(document, actualBgColor);
}
}
private List<TestTextMatch> getMatches() {
String searchText = findDialog.getSearchText();
List<TestTextMatch> results = new ArrayList<>();
String text = runSwing(() -> textPane.getText());
int index = text.indexOf(searchText);
while (index != -1) {
results.add(new TestTextMatch(index, index + searchText.length()));
index = text.indexOf(searchText, index + 1);
}
return results;
}
private void find(String text) {
runSwing(() -> findDialog.setSearchText(text));
pressButtonByText(findDialog, "Next");
waitForTasks();
}
private FindDialog showFindDialog() {
DockingActionIf action = getAction(tool, "ConsolePlugin", "Find");
performAction(action, false);
return waitForDialogComponent(FindDialog.class);
}
private record TestTextMatch(int start, int end) {
boolean contains(int caret) {
return start <= caret && caret <= end;
}
@Override
public String toString() {
return "[" + start + ',' + end + ']';
}
}
}

View file

@ -21,14 +21,13 @@ import java.util.function.Supplier;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.text.Document;
import org.junit.Test;
import generic.test.AbstractGuiTest;
import ghidra.framework.plugintool.DummyPluginTool;
public class ConsoleTextPaneTest {
public class ConsoleTextPaneTest extends AbstractGuiTest {
private int runNumber = 1;
@ -104,34 +103,36 @@ public class ConsoleTextPaneTest {
assertCaretAtBottom(text);
}
//=================================================================================================
// Private Methods
//=================================================================================================
private void setCaret(ConsoleTextPane text, int position) {
swing(() -> text.setCaretPosition(position));
}
private void assertCaretAtTop(ConsoleTextPane text) {
AbstractGuiTest.waitForSwing();
waitForSwing();
int expectedPosition = 0;
assertCaretPosition(text, expectedPosition);
}
private void assertCaretAtBottom(ConsoleTextPane text) {
AbstractGuiTest.waitForSwing();
waitForSwing();
int expectedPosition = text.getDocument().getLength();
assertCaretPosition(text, expectedPosition);
}
private void assertCaretPosition(ConsoleTextPane text, int expectedPosition) {
AbstractGuiTest.waitForSwing();
Document doc = text.getDocument();
waitForSwing();
int actualPosition = swing(() -> text.getCaretPosition());
assertEquals(expectedPosition, actualPosition);
}
private void printEnoughLinesToOverflowTheMaxCharCount(ConsoleTextPane text) {
AbstractGuiTest.runSwing(() -> {
runSwing(() -> {
int charsWritten = 0;
for (int i = 0; charsWritten < text.getMaximumCharacterLimit(); i++) {
@ -145,10 +146,10 @@ public class ConsoleTextPaneTest {
}
private void swing(Runnable r) {
AbstractGuiTest.runSwing(r);
runSwing(r);
}
private <T> T swing(Supplier<T> s) {
return AbstractGuiTest.runSwing(s);
return runSwing(s);
}
}

View file

@ -35,11 +35,11 @@ import ghidra.util.datastruct.Duo.Side;
public class DecompilerDiffViewFindAction extends DockingAction {
private Duo<FindDialog> findDialogs;
private Duo<FindDialog> findDialogs = new Duo<>();
private PluginTool tool;
public DecompilerDiffViewFindAction(String owner, PluginTool tool) {
super("Find", owner, true);
super("Find", owner, KeyBindingType.SHARED);
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
setPopupMenuData(new MenuData(new String[] { "Find..." }, "Decompile"));
setKeyBindingData(
@ -48,6 +48,12 @@ public class DecompilerDiffViewFindAction extends DockingAction {
this.tool = tool;
}
@Override
public void dispose() {
super.dispose();
findDialogs.each(dialog -> dialog.dispose());
}
@Override
public boolean isAddToPopup(ActionContext context) {
return (context instanceof DualDecompilerActionContext);

View file

@ -15,6 +15,7 @@
*/
package ghidra.app.decompiler.component;
import java.awt.Component;
import java.util.List;
import java.util.stream.Collectors;
@ -26,10 +27,10 @@ import docking.widgets.FindDialog;
import docking.widgets.SearchLocation;
import docking.widgets.button.GButton;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.table.AbstractDynamicTableColumnStub;
import docking.widgets.table.TableColumnDescriptor;
import docking.widgets.table.*;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearcher;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.table.TableComponentProvider;
import ghidra.app.util.HelpTopics;
import ghidra.app.util.query.TableService;
@ -43,11 +44,14 @@ import ghidra.util.Msg;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.*;
import ghidra.util.table.column.AbstractGhidraColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
public class DecompilerFindDialog extends FindDialog {
private DecompilerPanel decompilerPanel;
private GButton showAllButton;
public DecompilerFindDialog(DecompilerPanel decompilerPanel) {
super("Decompiler Find Text", new DecompilerSearcher(decompilerPanel));
@ -55,7 +59,7 @@ public class DecompilerFindDialog extends FindDialog {
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
GButton showAllButton = new GButton("Search All");
showAllButton = new GButton("Search All");
showAllButton.addActionListener(e -> showAll());
// move this button to the end
@ -65,7 +69,16 @@ public class DecompilerFindDialog extends FindDialog {
addButton(dismissButton);
}
@Override
protected void enableButtons(boolean b) {
super.enableButtons(b);
showAllButton.setEnabled(b);
}
private void showAll() {
String searchText = getSearchText();
close();
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
@ -78,7 +91,7 @@ public class DecompilerFindDialog extends FindDialog {
return;
}
List<SearchLocation> results = searcher.searchAll(getSearchText(), useRegex());
List<SearchLocation> results = searcher.searchAll(searchText, useRegex());
if (!results.isEmpty()) {
// save off searches that find results so users can reuse them later
storeSearchText(getSearchText());
@ -127,12 +140,6 @@ public class DecompilerFindDialog extends FindDialog {
provider.setTabText("'%s'".formatted(getSearchText()));
}
@Override
protected void dialogClosed() {
// clear the search results when the dialog is closed
decompilerPanel.setSearchResults(null);
}
//=================================================================================================
// Inner Classes
//=================================================================================================
@ -202,18 +209,57 @@ public class DecompilerFindDialog extends FindDialog {
}
private class ContextColumn
extends AbstractDynamicTableColumnStub<DecompilerSearchLocation, String> {
extends
AbstractDynamicTableColumnStub<DecompilerSearchLocation, LocationReferenceContext> {
private GColumnRenderer<LocationReferenceContext> renderer = new ContextCellRenderer();
@Override
public String getValue(DecompilerSearchLocation rowObject, Settings settings,
public LocationReferenceContext getValue(DecompilerSearchLocation rowObject,
Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getTextLine();
LocationReferenceContext context = rowObject.getContext();
return context;
// return rowObject.getTextLine();
}
@Override
public String getColumnName() {
return "Context";
}
@Override
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
return renderer;
}
private class ContextCellRenderer
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
{
// the context uses html
setHTMLRenderingEnabled(true);
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
// initialize
super.getTableCellRendererComponent(data);
DecompilerSearchLocation match = (DecompilerSearchLocation) data.getRowObject();
LocationReferenceContext context = match.getContext();
String text = context.getBoldMatchingText();
setText(text);
return this;
}
@Override
public String getFilterString(LocationReferenceContext context, Settings settings) {
return context.getPlainText();
}
}
}
}
}

View file

@ -18,18 +18,22 @@ package ghidra.app.plugin.core.decompile.actions;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
public class DecompilerSearchLocation extends SearchLocation {
private final FieldLocation fieldLocation;
private String textLine;
private LocationReferenceContext context;
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
int endIndexInclusive, String searchText, boolean forwardDirection, String textLine) {
int endIndexInclusive, String searchText, boolean forwardDirection, String textLine,
LocationReferenceContext context) {
super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection);
this.fieldLocation = fieldLocation;
this.textLine = textLine;
this.context = context;
}
public FieldLocation getFieldLocation() {
@ -40,6 +44,10 @@ public class DecompilerSearchLocation extends SearchLocation {
return textLine;
}
public LocationReferenceContext getContext() {
return context;
}
@Override
public CursorPosition getCursorPosition() {
return new DecompilerCursorPosition(fieldLocation);

View file

@ -25,6 +25,8 @@ import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.RowColLocation;
import ghidra.app.decompiler.component.ClangTextField;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
@ -84,6 +86,16 @@ public class DecompilerSearcher implements FindDialogSearcher {
decompilerPanel.setSearchResults(location);
}
@Override
public void clearHighlights() {
decompilerPanel.setSearchResults(null);
}
@Override
public void dispose() {
clearHighlights();
}
@Override
public SearchLocation search(String text, CursorPosition position, boolean searchForward,
boolean useRegex) {
@ -160,7 +172,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
results.add(searchLocation);
FieldLocation last = searchLocation.getFieldLocation();
int line = last.getIndex().intValue();
int field = 0; // there is only 1 field
int row = 0; // there is only 1 row
@ -260,6 +271,8 @@ public class DecompilerSearcher implements FindDialogSearcher {
if (match == SearchMatch.NO_MATCH) {
continue;
}
String fullLine = field.getText();
if (i == line) { // cursor is on this line
//
// The match start for all lines without the cursor will be relative to the start
@ -267,7 +280,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
// the match start is relative to the cursor position. Update the start to
// compensate for the difference between the start of the line and the cursor.
//
String fullLine = field.getText();
int cursorOffset = fullLine.length() - partialLine.length();
match.start += cursorOffset;
match.end += cursorOffset;
@ -276,13 +288,26 @@ public class DecompilerSearcher implements FindDialogSearcher {
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
LocationReferenceContext context = createContext(fullLine, match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true, field.getText());
searchString, true, field.getText(), context);
}
return null;
}
private LocationReferenceContext createContext(String line, SearchMatch match) {
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
int start = match.start;
int end = match.end;
builder.append(line.substring(0, start));
builder.appendMatch(line.substring(start, end));
if (end < line.length()) {
builder.append(line.substring(end));
}
return builder.build();
}
private DecompilerSearchLocation findPrevious(Function<String, SearchMatch> matcher,
String searchString, FieldLocation currentLocation) {
@ -291,16 +316,17 @@ public class DecompilerSearcher implements FindDialogSearcher {
for (int i = line; i >= 0; i--) {
ClangTextField field = (ClangTextField) fields.get(i);
String textLine = substring(field, (i == line) ? currentLocation : null, false);
SearchMatch match = matcher.apply(textLine);
if (match != SearchMatch.NO_MATCH) {
if (match == SearchMatch.NO_MATCH) {
continue;
}
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
LocationReferenceContext context = createContext(field.getText(), match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, false, field.getText());
}
searchString, false, field.getText(), context);
}
return null;
}
@ -317,7 +343,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
}
String partialText = textField.getText();
if (forwardSearch) {
int nextCol = location.getCol();
@ -365,6 +390,5 @@ public class DecompilerSearcher implements FindDialogSearcher {
}
}
private record FieldLineLocation(int fieldNumber, int column) {
}
private record FieldLineLocation(int fieldNumber, int column) {}
}

View file

@ -20,8 +20,7 @@ import java.awt.event.KeyEvent;
import org.apache.commons.lang3.StringUtils;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import docking.action.*;
import docking.widgets.FindDialog;
import ghidra.app.decompiler.component.DecompilerFindDialog;
import ghidra.app.decompiler.component.DecompilerPanel;
@ -40,6 +39,11 @@ public class FindAction extends AbstractDecompilerAction {
setEnabled(true);
}
@Override
public KeyBindingType getKeyBindingType() {
return KeyBindingType.SHARED;
}
@Override
public void dispose() {
if (findDialog != null) {

View file

@ -22,6 +22,9 @@ color.bg.highlight = color.palette.lemonchiffon
color.bg.currentline = color.palette.aliceblue
color.bg.find.highlight = color.palette.yellow
color.bg.find.highlight.active = color.palette.orange
color.bg.textfield.hint.valid = color.bg
color.bg.textfield.hint.invalid = color.palette.mistyrose
color.fg.textfield.hint = color.fg.messages.hint
@ -184,7 +187,8 @@ font.wizard.border.title = sansserif-plain-10
color.fg.filterfield = color.palette.darkslategray
color.bg.highlight = #703401 // orangish
color.bg.highlight = #67582A // olivish
color.bg.find.highlight.active = #A24E05 // orangish
color.bg.filechooser.shortcut = [color]system.color.bg.view

View file

@ -725,6 +725,12 @@ public class DialogComponentProvider
return;
}
Callback animatorFinishedCallback = () -> {
statusLabel.setVisible(true);
alertFinishedCallback.call();
isAlerting = false;
};
isAlerting = true;
// Note: manually call validate() so the 'statusLabel' updates its bounds after
@ -733,15 +739,18 @@ public class DialogComponentProvider
mainPanel.validate();
statusLabel.setVisible(false); // disable painting in this dialog so we don't see double
Animator animator = AnimationUtils.pulseComponent(statusLabel, 1);
if (animator == null) {
animatorFinishedCallback.call();
}
else {
animator.addTarget(new TimingTargetAdapter() {
@Override
public void end() {
statusLabel.setVisible(true);
alertFinishedCallback.call();
isAlerting = false;
animatorFinishedCallback.call();
}
});
}
}
protected Color getStatusColor(MessageType type) {
switch (type) {

View file

@ -101,7 +101,7 @@ public class DockingHelpBroker extends GHelpBroker {
@Override
protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) {
helpModel.addHelpModelListener(helpModelListener);
new HelpViewSearcher(jHelp, helpModel);
new HelpViewSearcher(jHelp);
}
@Override

View file

@ -18,27 +18,21 @@ package docking.help;
import java.awt.Component;
import java.awt.Window;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.regex.*;
import java.util.Enumeration;
import javax.help.*;
import javax.help.DefaultHelpModel.DefaultHighlight;
import javax.help.search.SearchEngine;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import docking.DockingUtils;
import docking.DockingWindowManager;
import docking.actions.KeyBindingUtils;
import docking.widgets.*;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.util.WindowUtilities;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;
import ghidra.util.task.*;
/**
* Enables the Find Dialog for searching through the current page of a help document.
@ -51,49 +45,19 @@ class HelpViewSearcher {
private static KeyStroke FIND_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F, DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private Comparator<SearchHit> searchResultComparator =
(o1, o2) -> o1.getBegin() - o2.getBegin();
private Comparator<? super SearchHit> searchResultReverseComparator =
(o1, o2) -> o2.getBegin() - o1.getBegin();
private JHelp jHelp;
private SearchEngine searchEngine;
private HelpModel helpModel;
private JEditorPane htmlEditorPane;
private FindDialog findDialog;
private boolean startSearchFromBeginning;
private boolean settingHighlights;
HelpViewSearcher(JHelp jHelp, HelpModel helpModel) {
HelpViewSearcher(JHelp jHelp) {
this.jHelp = jHelp;
this.helpModel = helpModel;
findDialog = new FindDialog(DIALOG_TITLE_PREFIX, new Searcher()) {
@Override
public void close() {
super.close();
clearHighlights();
}
};
// URL startURL = helpModel.getCurrentURL();
// if (isValidHelpURL(startURL)) {
// currentPageURL = startURL;
// }
grabSearchEngine();
JHelpContentViewer contentViewer = jHelp.getContentViewer();
contentViewer.addTextHelpModelListener(e -> {
if (settingHighlights) {
return; // ignore our changes
}
clearSearchState();
});
contentViewer.addHelpModelListener(e -> {
URL url = e.getURL();
@ -102,24 +66,22 @@ class HelpViewSearcher {
return;
}
// currentPageURL = url;
String file = url.getFile();
int separatorIndex = file.lastIndexOf(File.separator);
file = file.substring(separatorIndex + 1);
findDialog.setTitle(DIALOG_TITLE_PREFIX + file);
clearSearchState(); // new page
});
// note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration
htmlEditorPane = getHTMLEditorPane(contentViewer);
TextComponentSearcher searcher = new TextComponentSearcher(htmlEditorPane);
findDialog = new FindDialog(DIALOG_TITLE_PREFIX, searcher);
htmlEditorPane.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
htmlEditorPane.getCaret().setVisible(true);
startSearchFromBeginning = false;
}
});
@ -214,14 +176,6 @@ class HelpViewSearcher {
return (JEditorPane) viewport.getView();
}
private void clearSearchState() {
startSearchFromBeginning = true;
}
private void clearHighlights() {
((TextHelpModel) helpModel).removeAllHighlights();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
@ -239,156 +193,6 @@ class HelpViewSearcher {
}
}
private class Searcher implements FindDialogSearcher {
@Override
public CursorPosition getCursorPosition() {
if (startSearchFromBeginning) {
startSearchFromBeginning = false;
return new CursorPosition(0);
}
int caretPosition = htmlEditorPane.getCaretPosition();
return new CursorPosition(caretPosition);
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
}
@Override
public CursorPosition getEnd() {
int length = htmlEditorPane.getDocument().getLength();
return new CursorPosition(length - 1);
}
@Override
public void setCursorPosition(CursorPosition position) {
int cursorPosition = position.getPosition();
htmlEditorPane.setCaretPosition(cursorPosition);
}
@Override
public void highlightSearchResults(SearchLocation location) {
if (location == null) {
((TextHelpModel) helpModel).setHighlights(new DefaultHighlight[0]);
return;
}
int start = location.getStartIndexInclusive();
DefaultHighlight[] h = new DefaultHighlight[] {
new DefaultHighlight(start, location.getEndIndexInclusive()) };
// using setHighlights() instead of removeAll + add
// avoids one highlighting event
try {
settingHighlights = true;
((TextHelpModel) helpModel).setHighlights(h);
htmlEditorPane.getCaret().setVisible(true); // bug
}
finally {
settingHighlights = false;
}
try {
Rectangle2D rectangle = htmlEditorPane.modelToView2D(start);
htmlEditorPane.scrollRectToVisible(rectangle.getBounds());
}
catch (BadLocationException e) {
// shouldn't happen
}
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
ScreenSearchTask searchTask = new ScreenSearchTask(text, useRegex);
new TaskLauncher(searchTask, htmlEditorPane);
List<SearchHit> searchResults = searchTask.getSearchResults();
int position = cursorPosition.getPosition(); // move to the next item
if (searchForward) {
Collections.sort(searchResults, searchResultComparator);
for (SearchHit searchHit : searchResults) {
int begin = searchHit.getBegin();
if (begin <= position) {
continue;
}
return new SearchLocation(begin, searchHit.getEnd(), text, searchForward);
}
}
else {
Collections.sort(searchResults, searchResultReverseComparator);
for (SearchHit searchHit : searchResults) {
int begin = searchHit.getBegin();
if (begin >= position) {
continue;
}
return new SearchLocation(begin, searchHit.getEnd(), text, searchForward);
}
}
return null; // no more matches in the current direction
}
}
private class ScreenSearchTask extends Task {
private String text;
private List<SearchHit> searchHits = new ArrayList<>();
private boolean useRegex;
ScreenSearchTask(String text, boolean useRegex) {
super("Help Search Task", true, false, true, true);
this.text = text;
this.useRegex = useRegex;
}
@Override
public void run(TaskMonitor monitor) {
Document document = htmlEditorPane.getDocument();
try {
String screenText = document.getText(0, document.getLength());
if (useRegex) {
Pattern pattern =
Pattern.compile(text, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
Matcher matcher = pattern.matcher(screenText);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
searchHits.add(new SearchHit(1D, start, end));
}
}
else {
int start = 0;
int wordOffset = text.length();
while (wordOffset < document.getLength()) {
String searchFor = screenText.substring(start, wordOffset);
if (text.compareToIgnoreCase(searchFor) == 0) { //Case insensitive
searchHits.add(new SearchHit(1D, start, wordOffset));
}
start++;
wordOffset++;
}
}
}
catch (BadLocationException e) {
// shouldn't happen
Msg.debug(this, "Unexpected exception retrieving help text", e);
}
catch (PatternSyntaxException e) {
Msg.showError(this, htmlEditorPane, "Regular Expression Syntax Error",
e.getMessage());
}
}
List<SearchHit> getSearchResults() {
return searchHits;
}
}
//
// private class IndexerSearchTask extends Task {
//

View file

@ -27,10 +27,14 @@ import docking.ReusableDialogComponentProvider;
import docking.widgets.button.GRadioButton;
import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.label.GLabel;
import utility.function.Callback;
/**
* A dialog used to perform text searches on a text display.
*/
public class FindDialog extends ReusableDialogComponentProvider {
private GhidraComboBox<String> comboBox;
protected GhidraComboBox<String> comboBox;
protected FindDialogSearcher searcher;
private JButton nextButton;
@ -38,6 +42,8 @@ public class FindDialog extends ReusableDialogComponentProvider {
private JRadioButton stringRadioButton;
private JRadioButton regexRadioButton;
private Callback closedCallback = Callback.dummy();
public FindDialog(String title, FindDialogSearcher searcher) {
super(title, false, true, true, true);
this.searcher = searcher;
@ -46,6 +52,16 @@ public class FindDialog extends ReusableDialogComponentProvider {
buildButtons();
}
@Override
public void dispose() {
searcher.dispose();
super.dispose();
}
public void setClosedCallback(Callback c) {
this.closedCallback = Callback.dummyIfNull(c);
}
private void buildButtons() {
nextButton = new JButton("Next");
nextButton.setMnemonic('N');
@ -113,7 +129,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
return mainPanel;
}
private void enableButtons(boolean b) {
protected void enableButtons(boolean b) {
nextButton.setEnabled(b);
previousButton.setEnabled(b);
}
@ -130,6 +146,8 @@ public class FindDialog extends ReusableDialogComponentProvider {
@Override
protected void dialogClosed() {
comboBox.setText("");
searcher.clearHighlights();
closedCallback.call();
}
public void next() {
@ -206,7 +224,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
// -don't allow searching again while notifying
// -make sure the user can see it
enableButtons(false);
alertMessage(() -> enableButtons(true));
alertMessage(() -> {
String text = comboBox.getText();
enableButtons(text.length() != 0);
});
}
@Override
@ -214,6 +235,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
clearStatusText();
}
public FindDialogSearcher getSearcher() {
return searcher;
}
String getText() {
if (isVisible()) {
return comboBox.getText();

View file

@ -60,6 +60,11 @@ public interface FindDialogSearcher {
*/
public void highlightSearchResults(SearchLocation location);
/**
* Clears any active highlights.
*/
public void clearHighlights();
/**
* Perform a search for the next item in the given direction starting at the given cursor
* position.
@ -83,4 +88,11 @@ public interface FindDialogSearcher {
public default List<SearchLocation> searchAll(String text, boolean useRegex) {
throw new UnsupportedOperationException("Search All is not defined for this searcher");
}
/**
* Disposes this searcher. This does nothing by default.
*/
public default void dispose() {
// stub
}
}

View file

@ -0,0 +1,536 @@
/* ###
* 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.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.*;
import javax.swing.JEditorPane;
import javax.swing.event.*;
import javax.swing.text.*;
import generic.theme.GColor;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
/**
* A class to find text matches in the given {@link TextComponent}. This class will search for all
* matches and cache the results for future requests when the user presses Next or Previous. All
* matches will be highlighted in the text component. The match containing the cursor will be a
* different highlight color than the others. When the find dialog is closed, all highlights are
* removed.
*/
public class TextComponentSearcher implements FindDialogSearcher {
private Color highlightColor = new GColor("color.bg.find.highlight");
private Color activeHighlightColor = new GColor("color.bg.find.highlight.active");
private JEditorPane editorPane;
private DocumentListener documentListener = new DocumentChangeListener();
private CaretListener caretListener = new CaretChangeListener();
private SwingUpdateManager caretUpdater = new SwingUpdateManager(() -> updateActiveHighlight());
private volatile boolean isUpdatingCaretInternally;
private SearchResults searchResults;
public TextComponentSearcher(JEditorPane editorPane) {
this.editorPane = editorPane;
if (editorPane == null) {
return; // some clients initialize without an editor pane
}
Document document = editorPane.getDocument();
document.addDocumentListener(documentListener);
editorPane.addCaretListener(caretListener);
}
public void setEditorPane(JEditorPane editorPane) {
if (this.editorPane != editorPane) {
markResultsStale();
}
this.editorPane = editorPane;
}
public JEditorPane getEditorPane() {
return editorPane;
}
@Override
public void dispose() {
caretUpdater.dispose();
Document document = editorPane.getDocument();
document.removeDocumentListener(documentListener);
clearHighlights();
}
@Override
public void clearHighlights() {
if (searchResults != null) {
searchResults.removeHighlights();
searchResults = null;
}
}
public boolean hasSearchResults() {
return searchResults != null && !searchResults.isEmpty();
}
public boolean isStale() {
return searchResults != null && searchResults.isStale();
}
private void markResultsStale() {
if (searchResults != null) {
searchResults.setStale();
}
}
private void updateActiveHighlight() {
if (searchResults == null) {
return;
}
int pos = editorPane.getCaretPosition();
searchResults.updateActiveMatch(pos);
}
private void setCaretPositionInternally(int pos) {
isUpdatingCaretInternally = true;
try {
editorPane.setCaretPosition(pos);
}
finally {
isUpdatingCaretInternally = false;
}
}
@Override
public CursorPosition getCursorPosition() {
int pos = editorPane.getCaretPosition();
return new CursorPosition(pos);
}
@Override
public void setCursorPosition(CursorPosition position) {
int pos = position.getPosition();
editorPane.setCaretPosition(pos);
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
}
@Override
public CursorPosition getEnd() {
int length = editorPane.getDocument().getLength();
return new CursorPosition(length - 1);
}
@Override
public void highlightSearchResults(SearchLocation location) {
if (location == null) {
clearHighlights();
return;
}
TextComponentSearchLocation textLocation = (TextComponentSearchLocation) location;
FindMatch match = textLocation.getMatch();
searchResults.setActiveMatch(match);
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
updateSearchResults(text, useRegex);
int pos = cursorPosition.getPosition();
int searchStart = getSearchStart(pos, searchForward);
FindMatch match = searchResults.getNextMatch(searchStart, searchForward);
if (match == null) {
return null;
}
return new TextComponentSearchLocation(match.getStart(), match.getEnd(), text,
searchForward, match);
}
private void updateSearchResults(String text, boolean useRegex) {
if (searchResults != null) {
if (!searchResults.isInvalid(text)) {
return; // the current results are still valid
}
searchResults.removeHighlights();
}
SearchTask searchTask = new SearchTask(text, useRegex);
TaskLauncher.launch(searchTask);
searchResults = searchTask.getSearchResults();
searchResults.applyHighlights();
}
private int getSearchStart(int startPosition, boolean isForward) {
FindMatch activeMatch = searchResults.getActiveMatch();
if (activeMatch == null) {
return startPosition;
}
int lastMatchStart = activeMatch.getStart();
if (startPosition != lastMatchStart) {
return startPosition;
}
// Always prefer the caret position, unless it aligns with the previous match. By
// moving it forward one we will continue our search, as opposed to always matching
// the same hit.
if (isForward) {
return startPosition + 1;
}
// backwards
if (startPosition == 0) {
return editorPane.getText().length();
}
return startPosition - 1;
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class SearchResults {
private TreeMap<Integer, FindMatch> matchesByPosition;
private FindMatch activeMatch;
private boolean isStale;
private String searchText;
SearchResults(String searchText, TreeMap<Integer, FindMatch> matchesByPosition) {
this.searchText = searchText;
this.matchesByPosition = matchesByPosition;
}
boolean isStale() {
return isStale;
}
void updateActiveMatch(int pos) {
if (activeMatch != null) {
activeMatch.setActive(false);
activeMatch = null;
}
if (isStale) {
// not way to easily change highlights for the caret position while we are stale,
// since the matches no longer match the document positions
return;
}
Iterator<FindMatch> it = matchesByPosition.values().iterator();
while (it.hasNext()) {
FindMatch match = it.next();
boolean isActive = false;
if (match.contains(pos)) {
activeMatch = match;
isActive = true;
}
match.setActive(isActive);
}
}
FindMatch getActiveMatch() {
return activeMatch;
}
FindMatch getNextMatch(int searchStart, boolean searchForward) {
Entry<Integer, FindMatch> entry;
if (searchForward) {
entry = matchesByPosition.ceilingEntry(searchStart);
}
else {
entry = matchesByPosition.floorEntry(searchStart);
}
if (entry == null) {
return null; // no more matches in the current direction
}
return entry.getValue();
}
boolean isEmpty() {
return matchesByPosition.isEmpty();
}
void setStale() {
isStale = true;
}
boolean isInvalid(String otherSearchText) {
if (isStale) {
return true;
}
return !searchText.equals(otherSearchText);
}
void setActiveMatch(FindMatch match) {
if (activeMatch != null) {
activeMatch.setActive(false);
}
activeMatch = match;
activeMatch.activate();
}
void applyHighlights() {
Collection<FindMatch> matches = matchesByPosition.values();
for (FindMatch match : matches) {
match.applyHighlight();
}
}
void removeHighlights() {
activeMatch = null;
JEditorPane editor = editorPane;
Highlighter highlighter = editor.getHighlighter();
if (highlighter != null) {
highlighter.removeAllHighlights();
}
matchesByPosition.clear();
}
}
private class TextComponentSearchLocation extends SearchLocation {
private FindMatch match;
public TextComponentSearchLocation(int start, int end,
String searchText, boolean forwardDirection, FindMatch match) {
super(start, end, searchText, forwardDirection);
this.match = match;
}
FindMatch getMatch() {
return match;
}
}
private class SearchTask extends Task {
private String searchText;
private TreeMap<Integer, FindMatch> searchHits = new TreeMap<>();
private boolean useRegex;
SearchTask(String searchText, boolean useRegex) {
super("Help Search Task", true, false, true, true);
this.searchText = searchText;
this.useRegex = useRegex;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
String screenText;
try {
Document document = editorPane.getDocument();
screenText = document.getText(0, document.getLength());
}
catch (BadLocationException e) {
Msg.error(this, "Unable to get text for user find operation", e);
return;
}
Pattern pattern = createSearchPattern(searchText, useRegex);
Matcher matcher = pattern.matcher(screenText);
while (matcher.find()) {
monitor.checkCancelled();
int start = matcher.start();
int end = matcher.end();
FindMatch match = new FindMatch(searchText, start, end);
searchHits.put(start, match);
}
}
private Pattern createSearchPattern(String searchString, boolean isRegex) {
int options = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
if (isRegex) {
try {
return Pattern.compile(searchString, options);
}
catch (PatternSyntaxException e) {
Msg.showError(this, editorPane, "Regular Expression Syntax Error",
e.getMessage());
return null;
}
}
return UserSearchUtils.createPattern(searchString, false, options);
}
SearchResults getSearchResults() {
return new SearchResults(searchText, searchHits);
}
}
private class FindMatch {
private String text;
private int start;
private int end;
private boolean isActive;
// this tag is a way to remove an installed highlight
private Object lastHighlightTag;
FindMatch(String text, int start, int end) {
this.start = start;
this.end = end;
this.text = text;
}
boolean contains(int pos) {
// exclusive of end so the cursor behind the match does is not in the highlight
return start <= pos && pos < end;
}
/** Calls setActive() and moves the caret position */
void activate() {
setActive(true);
setCaretPositionInternally(start);
scrollToVisible();
}
/**
* Makes this match active and updates the highlight color
* @param b true for active
*/
void setActive(boolean b) {
isActive = b;
applyHighlight();
}
int getStart() {
return start;
}
int getEnd() {
return end;
}
void scrollToVisible() {
try {
Rectangle startR = editorPane.modelToView2D(start).getBounds();
Rectangle endR = editorPane.modelToView2D(end).getBounds();
endR.width += 20; // a little extra space so the view is not right at the text end
Rectangle union = startR.union(endR);
editorPane.scrollRectToVisible(union);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception scrolling to text", e);
}
}
@Override
public String toString() {
return "[" + start + ',' + end + "] " + text;
}
void applyHighlight() {
Highlighter highlighter = editorPane.getHighlighter();
if (highlighter == null) {
highlighter = new DefaultHighlighter();
editorPane.setHighlighter(highlighter);
}
Highlighter.HighlightPainter painter =
new DefaultHighlighter.DefaultHighlightPainter(
isActive ? activeHighlightColor : highlightColor);
try {
if (lastHighlightTag != null) {
highlighter.removeHighlight(lastHighlightTag);
}
lastHighlightTag = highlighter.addHighlight(start, end, painter);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception adding highlight", e);
}
}
}
private class DocumentChangeListener implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
// this allows the previous search results to stay visible until a new find is requested
markResultsStale();
}
@Override
public void removeUpdate(DocumentEvent e) {
markResultsStale();
}
@Override
public void changedUpdate(DocumentEvent e) {
// ignore attribute changes since they don't affect the text content
}
}
private class CaretChangeListener implements CaretListener {
private int lastPos = -1;
@Override
public void caretUpdate(CaretEvent e) {
int pos = e.getDot();
if (isUpdatingCaretInternally) {
lastPos = pos;
return;
}
if (pos == lastPos) {
return;
}
lastPos = pos;
caretUpdater.update();
}
}
}

View file

@ -108,12 +108,16 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
@Override
public void setUI(ComboBoxUI ui) {
int oldColumns = getColumns();
super.setUI(ui);
// this gets called during construction and during theming changes. It always
// This gets called during construction and during theming changes. It always
// creates a new editor and any listeners or documents set on the current editor are
// lost. So to combat this, we install the pass through listeners here instead of
// in the init() method. We also reset the document if the client ever called the
// setDocument() method
// setDocument() method.
installPassThroughListeners();
@ -134,6 +138,13 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
}
});
}
// As mentioned above, the default editor gets replaced. In that case, restore the columns
// if the client has set the value.
if (oldColumns > 0) {
JTextField tf = getTextField();
tf.setColumns(oldColumns);
}
}
/**
@ -189,14 +200,36 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
*
* @param columnCount The number of columns for the text field editor
* @see JTextField#setColumns(int)
* @deprecated use {@link #setColumns(int)}
*/
@Deprecated(forRemoval = true, since = "11.3")
public void setColumnCount(int columnCount) {
JTextField textField = getTextField();
textField.setColumns(columnCount);
setColumns(columnCount);
}
/**
* Selects the text in the text field editor usd by this combo box.
* Sets the number of column's in the editor's component (JTextField).
* @param columns the number of columns to show
* @see JTextField#setColumns(int)
*/
public void setColumns(int columns) {
JTextField textField = getTextField();
textField.setColumns(columns);
}
private int getColumns() {
ComboBoxEditor currentEditor = getEditor();
if (currentEditor != null) {
Object object = currentEditor.getEditorComponent();
if (object instanceof JTextField textField) {
return textField.getColumns();
}
}
return -1;
}
/**
* Selects the text in the text field editor used by this combo box.
*
* @see JTextField#selectAll()
*/
@ -297,16 +330,6 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
docListeners.remove(l);
}
/**
* Sets the number of column's in the editor's component (JTextField).
* @param columns the number of columns to show
* @see JTextField#setColumns(int)
*/
public void setColumns(int columns) {
JTextField textField = getTextField();
textField.setColumns(columns);
}
/**
* Convenience method for associating a label with the editor component.
* @param label the label to associate

View file

@ -63,6 +63,11 @@ public class FindDialogTest {
// stub
}
@Override
public void clearHighlights() {
// stub
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {

View file

@ -473,12 +473,12 @@ public class AbstractGuiTest extends AbstractGenericTest {
public static AbstractButton findAbstractButtonByName(Container container, String name) {
Component[] comp = container.getComponents();
for (Component element : comp) {
if ((element instanceof AbstractButton) &&
name.equals(((AbstractButton) element).getName())) {
return (AbstractButton) element;
if ((element instanceof AbstractButton button) &&
name.equals(button.getName())) {
return button;
}
else if (element instanceof Container) {
AbstractButton b = findAbstractButtonByName((Container) element, name);
else if (element instanceof Container subContainer) {
AbstractButton b = findAbstractButtonByName(subContainer, name);
if (b != null) {
return b;
}