mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
Merge remote-tracking branch
'origin/GP-3858-dragonmacher-console-find--SQUASHED' (Closes #2567, #7136)
This commit is contained in:
commit
4db0ccc8ec
24 changed files with 1525 additions and 505 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 + ']';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
//
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue