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

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,11 +20,13 @@ import java.awt.event.*;
import java.io.PrintWriter; import java.io.PrintWriter;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.BadLocationException; import javax.swing.text.*;
import javax.swing.text.Document;
import docking.*; import docking.*;
import docking.action.*; import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.theme.GIcon; import generic.theme.GIcon;
import generic.theme.Gui; import generic.theme.Gui;
import ghidra.app.services.*; import ghidra.app.services.*;
@ -53,13 +55,19 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
private ConsoleTextPane textPane; private ConsoleTextPane textPane;
private JScrollPane scroller; private JScrollPane scroller;
private JComponent component; private JComponent component;
private boolean scrollLock = false;
private DockingAction clearAction;
private ToggleDockingAction scrollAction;
private Address currentAddress;
private PrintWriter stderr; private PrintWriter stderr;
private PrintWriter stdin; private PrintWriter stdin;
private boolean scrollLock = false;
private DockingAction clearAction;
private ToggleDockingAction scrollAction;
private Program currentProgram; private Program currentProgram;
private Address currentAddress;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public ConsoleComponentProvider(PluginTool tool, String owner) { public ConsoleComponentProvider(PluginTool tool, String owner) {
super(tool, "Console", owner); super(tool, "Console", owner);
@ -97,6 +105,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
textPane.dispose(); textPane.dispose();
stderr.close(); stderr.close();
stdin.close(); stdin.close();
if (findDialog != null) {
findDialog.close();
}
} }
private void createOptions() { private void createOptions() {
@ -117,70 +129,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
textPane.setName(textPaneName); textPane.setName(textPaneName);
textPane.getAccessibleContext().setAccessibleName(textPaneName); textPane.getAccessibleContext().setAccessibleName(textPaneName);
textPane.addMouseMotionListener(new MouseMotionAdapter() { textPane.addMouseMotionListener(new CursorUpdateMouseMotionListener());
@Override textPane.addMouseListener(new GoToMouseListener());
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);
}
});
scroller = new JScrollPane(textPane); scroller = new JScrollPane(textPane);
scroller.setPreferredSize(new Dimension(200, 100)); scroller.setPreferredSize(new Dimension(200, 100));
@ -191,74 +141,12 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
tool.addComponentProvider(this, true); 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) { private boolean isSymbol(String word) {
SymbolTable symbolTable = currentProgram.getSymbolTable(); SymbolTable symbolTable = currentProgram.getSymbolTable();
SymbolIterator symbolIterator = symbolTable.getSymbols(word); SymbolIterator symbolIterator = symbolTable.getSymbols(word);
return symbolIterator.hasNext(); 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() { private void createActions() {
clearAction = new DockingAction("Clear Console", getOwner()) { clearAction = new DockingAction("Clear Console", getOwner()) {
@ -268,9 +156,7 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
} }
}; };
clearAction.setDescription("Clear Console"); clearAction.setDescription("Clear Console");
// ACTIONS - auto generated
clearAction.setToolBarData(new ToolBarData(new GIcon("icon.plugin.console.clear"), null)); clearAction.setToolBarData(new ToolBarData(new GIcon("icon.plugin.console.clear"), null));
clearAction.setEnabled(true); clearAction.setEnabled(true);
scrollAction = new ToggleDockingAction("Scroll Lock", getOwner()) { scrollAction = new ToggleDockingAction("Scroll Lock", getOwner()) {
@ -282,14 +168,33 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
scrollAction.setDescription("Scroll Lock"); scrollAction.setDescription("Scroll Lock");
scrollAction.setToolBarData( scrollAction.setToolBarData(
new ToolBarData(new GIcon("icon.plugin.console.scroll.lock"), null)); new ToolBarData(new GIcon("icon.plugin.console.scroll.lock"), null));
scrollAction.setEnabled(true); scrollAction.setEnabled(true);
scrollAction.setSelected(scrollLock); 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(scrollAction);
addLocalAction(clearAction); addLocalAction(clearAction);
} }
private void showFindDialog() {
if (findDialog == null) {
searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Find", searcher);
}
getTool().showDialog(findDialog);
}
@Override @Override
public void addMessage(String originator, String message) { public void addMessage(String originator, String message) {
checkVisible(); checkVisible();
@ -304,26 +209,6 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
@Override @Override
public void addException(String originator, Exception e) { 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); Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
} }
@ -331,6 +216,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
public void clearMessages() { public void clearMessages() {
checkVisible(); checkVisible();
textPane.setText(""); textPane.setText("");
if (searcher != null) {
searcher.clearHighlights();
}
} }
@Override @Override
@ -383,22 +272,21 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
return textPane.getDocument().getLength(); return textPane.getDocument().getLength();
} }
////////////////////////////////////////////////////////////////////
private void checkVisible() { private void checkVisible() {
if (!isVisible()) { if (!isVisible()) {
tool.showComponentProvider(this, true); tool.showComponentProvider(this, true);
} }
} }
/**
* @see docking.ComponentProvider#getComponent()
*/
@Override @Override
public JComponent getComponent() { public JComponent getComponent() {
return component; return component;
} }
ConsoleTextPane getTextPane() {
return textPane;
}
public void setCurrentProgram(Program program) { public void setCurrentProgram(Program program) {
currentProgram = program; currentProgram = program;
} }
@ -407,4 +295,136 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
currentAddress = address; 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

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -45,7 +45,6 @@ class FlowArrowPanel extends JPanel {
private Point pendingMouseClickPoint; private Point pendingMouseClickPoint;
FlowArrowPanel(FlowArrowPlugin p) { FlowArrowPanel(FlowArrowPlugin p) {
super();
this.plugin = p; this.plugin = p;
setMinimumSize(new Dimension(0, 0)); setMinimumSize(new Dimension(0, 0));
setPreferredSize(new Dimension(32, 1)); setPreferredSize(new Dimension(32, 1));
@ -164,7 +163,6 @@ class FlowArrowPanel extends JPanel {
ScrollingCallback callback = new ScrollingCallback(start, end); ScrollingCallback callback = new ScrollingCallback(start, end);
Animator animator = AnimationUtils.executeSwingAnimationCallback(callback); Animator animator = AnimationUtils.executeSwingAnimationCallback(callback);
callback.setAnimator(animator); callback.setAnimator(animator);
} }
private void processSingleClick(Point point) { private void processSingleClick(Point point) {
@ -322,7 +320,9 @@ class FlowArrowPanel extends JPanel {
if (current.equals(end)) { if (current.equals(end)) {
// we are done! // we are done!
animator.stop(); if (animator != null) {
animator.stop();
}
return; return;
} }

View file

@ -19,13 +19,13 @@ import java.io.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.swing.Icon; import javax.swing.*;
import javax.swing.JComponent;
import docking.ActionContext; import docking.ActionContext;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.action.ToolBarData; import docking.action.ToolBarData;
import docking.widgets.OptionDialog; import docking.action.builder.ActionBuilder;
import docking.widgets.*;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
@ -39,6 +39,9 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
private InterpreterConnection interpreter; private InterpreterConnection interpreter;
private List<Callback> firstActivationCallbacks; private List<Callback> firstActivationCallbacks;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public InterpreterComponentProvider(InterpreterPanelPlugin plugin, public InterpreterComponentProvider(InterpreterPanelPlugin plugin,
InterpreterConnection interpreter, boolean visible) { InterpreterConnection interpreter, boolean visible) {
super(plugin.getTool(), interpreter.getTitle(), interpreter.getTitle()); super(plugin.getTool(), interpreter.getTitle(), interpreter.getTitle());
@ -72,8 +75,28 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
clearAction.setDescription("Clear Interpreter"); clearAction.setDescription("Clear Interpreter");
clearAction.setToolBarData(new ToolBarData(Icons.CLEAR_ICON, null)); clearAction.setToolBarData(new ToolBarData(Icons.CLEAR_ICON, null));
clearAction.setEnabled(true); clearAction.setEnabled(true);
addLocalAction(clearAction); 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 @Override
@ -128,6 +151,10 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
@Override @Override
public void clear() { public void clear() {
panel.clear(); panel.clear();
if (searcher != null) {
searcher.clearHighlights();
}
} }
@Override @Override

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -77,7 +77,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
private SimpleAttributeSet STDIN_SET; private SimpleAttributeSet STDIN_SET;
private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB; private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB;
private boolean highlightCompletion = false;
private int completionInsertionPosition; private int completionInsertionPosition;
private boolean caretGuard = true; private boolean caretGuard = true;
@ -298,12 +297,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
completionWindowTrigger = completionWindowTrigger =
options.getEnum(COMPLETION_WINDOW_TRIGGER_LABEL, CompletionWindowTrigger.TAB); 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); options.addOptionsChangeListener(this);
} }
@ -317,10 +310,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
else if (optionName.equals(COMPLETION_WINDOW_TRIGGER_LABEL)) { else if (optionName.equals(COMPLETION_WINDOW_TRIGGER_LABEL)) {
completionWindowTrigger = (CompletionWindowTrigger) newValue; completionWindowTrigger = (CompletionWindowTrigger) newValue;
} }
// TODO
// else if (optionName.equals(HIGHLIGHT_COMPLETION_OPTION_LABEL)) {
// highlightCompletion = ((Boolean) newValue).booleanValue();
// }
} }
@Override @Override
@ -480,6 +469,10 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
stdin.resetStream(); stdin.resetStream();
} }
public JTextPane getOutputTextPane() {
return outputTextPane;
}
public String getOutputText() { public String getOutputText() {
return outputTextPane.getText(); return outputTextPane.getText();
} }
@ -535,16 +528,8 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
text.substring(0, insertedTextStart) + insertion + text.substring(position); text.substring(0, insertedTextStart) + insertion + text.substring(position);
setInputTextPaneText(inputText); setInputTextPaneText(inputText);
/* Select what we inserted so that the user can easily /* Then put the caret right after what we inserted. */
* get rid of what they did (in case of a mistake). */ inputTextPane.setCaretPosition(insertedTextEnd);
if (highlightCompletion) {
inputTextPane.setSelectionStart(insertedTextStart);
inputTextPane.moveCaretPosition(insertedTextEnd);
}
else {
/* Then put the caret right after what we inserted. */
inputTextPane.setCaretPosition(insertedTextEnd);
}
updateCompletionList(); updateCompletionList();
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -560,7 +560,7 @@ public class AddEditDialog extends ReusableDialogComponentProvider {
} }
}; };
// the number of columns determines the default width of the add/edit label dialog // 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"); labelNameChoices.setName("label.name.choices");
GhidraComboBox<NamespaceWrapper> comboBox = new GhidraComboBox<>(); GhidraComboBox<NamespaceWrapper> comboBox = new GhidraComboBox<>();
comboBox.setEnterKeyForwarding(true); comboBox.setEnterKeyForwarding(true);

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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. * 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 { 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 = new GAttributes(font, new GColor("color.fg.consoletextpane"));
outputAttributes.addAttribute(CUSTOM_ATTRIBUTE_KEY, OUTPUT_ATTRIBUTE_VALUE); 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); 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

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -21,14 +21,13 @@ import java.util.function.Supplier;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.text.Document;
import org.junit.Test; import org.junit.Test;
import generic.test.AbstractGuiTest; import generic.test.AbstractGuiTest;
import ghidra.framework.plugintool.DummyPluginTool; import ghidra.framework.plugintool.DummyPluginTool;
public class ConsoleTextPaneTest { public class ConsoleTextPaneTest extends AbstractGuiTest {
private int runNumber = 1; private int runNumber = 1;
@ -104,34 +103,36 @@ public class ConsoleTextPaneTest {
assertCaretAtBottom(text); assertCaretAtBottom(text);
} }
//=================================================================================================
// Private Methods
//=================================================================================================
private void setCaret(ConsoleTextPane text, int position) { private void setCaret(ConsoleTextPane text, int position) {
swing(() -> text.setCaretPosition(position)); swing(() -> text.setCaretPosition(position));
} }
private void assertCaretAtTop(ConsoleTextPane text) { private void assertCaretAtTop(ConsoleTextPane text) {
AbstractGuiTest.waitForSwing(); waitForSwing();
int expectedPosition = 0; int expectedPosition = 0;
assertCaretPosition(text, expectedPosition); assertCaretPosition(text, expectedPosition);
} }
private void assertCaretAtBottom(ConsoleTextPane text) { private void assertCaretAtBottom(ConsoleTextPane text) {
AbstractGuiTest.waitForSwing(); waitForSwing();
int expectedPosition = text.getDocument().getLength(); int expectedPosition = text.getDocument().getLength();
assertCaretPosition(text, expectedPosition); assertCaretPosition(text, expectedPosition);
} }
private void assertCaretPosition(ConsoleTextPane text, int expectedPosition) { private void assertCaretPosition(ConsoleTextPane text, int expectedPosition) {
waitForSwing();
AbstractGuiTest.waitForSwing();
Document doc = text.getDocument();
int actualPosition = swing(() -> text.getCaretPosition()); int actualPosition = swing(() -> text.getCaretPosition());
assertEquals(expectedPosition, actualPosition); assertEquals(expectedPosition, actualPosition);
} }
private void printEnoughLinesToOverflowTheMaxCharCount(ConsoleTextPane text) { private void printEnoughLinesToOverflowTheMaxCharCount(ConsoleTextPane text) {
AbstractGuiTest.runSwing(() -> { runSwing(() -> {
int charsWritten = 0; int charsWritten = 0;
for (int i = 0; charsWritten < text.getMaximumCharacterLimit(); i++) { for (int i = 0; charsWritten < text.getMaximumCharacterLimit(); i++) {
@ -145,10 +146,10 @@ public class ConsoleTextPaneTest {
} }
private void swing(Runnable r) { private void swing(Runnable r) {
AbstractGuiTest.runSwing(r); runSwing(r);
} }
private <T> T swing(Supplier<T> s) { private <T> T swing(Supplier<T> s) {
return AbstractGuiTest.runSwing(s); return runSwing(s);
} }
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -35,11 +35,11 @@ import ghidra.util.datastruct.Duo.Side;
public class DecompilerDiffViewFindAction extends DockingAction { public class DecompilerDiffViewFindAction extends DockingAction {
private Duo<FindDialog> findDialogs; private Duo<FindDialog> findDialogs = new Duo<>();
private PluginTool tool; private PluginTool tool;
public DecompilerDiffViewFindAction(String owner, PluginTool tool) { public DecompilerDiffViewFindAction(String owner, PluginTool tool) {
super("Find", owner, true); super("Find", owner, KeyBindingType.SHARED);
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind")); setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
setPopupMenuData(new MenuData(new String[] { "Find..." }, "Decompile")); setPopupMenuData(new MenuData(new String[] { "Find..." }, "Decompile"));
setKeyBindingData( setKeyBindingData(
@ -48,6 +48,12 @@ public class DecompilerDiffViewFindAction extends DockingAction {
this.tool = tool; this.tool = tool;
} }
@Override
public void dispose() {
super.dispose();
findDialogs.each(dialog -> dialog.dispose());
}
@Override @Override
public boolean isAddToPopup(ActionContext context) { public boolean isAddToPopup(ActionContext context) {
return (context instanceof DualDecompilerActionContext); return (context instanceof DualDecompilerActionContext);

View file

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

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,18 +18,22 @@ package ghidra.app.plugin.core.decompile.actions;
import docking.widgets.CursorPosition; import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation; import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
public class DecompilerSearchLocation extends SearchLocation { public class DecompilerSearchLocation extends SearchLocation {
private final FieldLocation fieldLocation; private final FieldLocation fieldLocation;
private String textLine; private String textLine;
private LocationReferenceContext context;
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive, 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); super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection);
this.fieldLocation = fieldLocation; this.fieldLocation = fieldLocation;
this.textLine = textLine; this.textLine = textLine;
this.context = context;
} }
public FieldLocation getFieldLocation() { public FieldLocation getFieldLocation() {
@ -40,6 +44,10 @@ public class DecompilerSearchLocation extends SearchLocation {
return textLine; return textLine;
} }
public LocationReferenceContext getContext() {
return context;
}
@Override @Override
public CursorPosition getCursorPosition() { public CursorPosition getCursorPosition() {
return new DecompilerCursorPosition(fieldLocation); return new DecompilerCursorPosition(fieldLocation);

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -25,6 +25,8 @@ import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.RowColLocation; import docking.widgets.fieldpanel.support.RowColLocation;
import ghidra.app.decompiler.component.ClangTextField; import ghidra.app.decompiler.component.ClangTextField;
import ghidra.app.decompiler.component.DecompilerPanel; 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.Msg;
import ghidra.util.UserSearchUtils; import ghidra.util.UserSearchUtils;
@ -84,6 +86,16 @@ public class DecompilerSearcher implements FindDialogSearcher {
decompilerPanel.setSearchResults(location); decompilerPanel.setSearchResults(location);
} }
@Override
public void clearHighlights() {
decompilerPanel.setSearchResults(null);
}
@Override
public void dispose() {
clearHighlights();
}
@Override @Override
public SearchLocation search(String text, CursorPosition position, boolean searchForward, public SearchLocation search(String text, CursorPosition position, boolean searchForward,
boolean useRegex) { boolean useRegex) {
@ -160,7 +172,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
results.add(searchLocation); results.add(searchLocation);
FieldLocation last = searchLocation.getFieldLocation(); FieldLocation last = searchLocation.getFieldLocation();
int line = last.getIndex().intValue(); int line = last.getIndex().intValue();
int field = 0; // there is only 1 field int field = 0; // there is only 1 field
int row = 0; // there is only 1 row int row = 0; // there is only 1 row
@ -260,14 +271,15 @@ public class DecompilerSearcher implements FindDialogSearcher {
if (match == SearchMatch.NO_MATCH) { if (match == SearchMatch.NO_MATCH) {
continue; continue;
} }
String fullLine = field.getText();
if (i == line) { // cursor is on this line if (i == line) { // cursor is on this line
// //
// The match start for all lines without the cursor will be relative to the start // The match start for all lines without the cursor will be relative to the start
// of the line, which is 0. However, when searching on the row with the cursor, // of the line, which is 0. However, when searching on the row with the cursor,
// the match start is relative to the cursor position. Update the start to // 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. // compensate for the difference between the start of the line and the cursor.
// //
String fullLine = field.getText();
int cursorOffset = fullLine.length() - partialLine.length(); int cursorOffset = fullLine.length() - partialLine.length();
match.start += cursorOffset; match.start += cursorOffset;
match.end += cursorOffset; match.end += cursorOffset;
@ -276,13 +288,26 @@ public class DecompilerSearcher implements FindDialogSearcher {
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field); FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation = FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column()); new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
LocationReferenceContext context = createContext(fullLine, match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1, return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true, field.getText()); searchString, true, field.getText(), context);
} }
return null; 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, private DecompilerSearchLocation findPrevious(Function<String, SearchMatch> matcher,
String searchString, FieldLocation currentLocation) { String searchString, FieldLocation currentLocation) {
@ -291,16 +316,17 @@ public class DecompilerSearcher implements FindDialogSearcher {
for (int i = line; i >= 0; i--) { for (int i = line; i >= 0; i--) {
ClangTextField field = (ClangTextField) fields.get(i); ClangTextField field = (ClangTextField) fields.get(i);
String textLine = substring(field, (i == line) ? currentLocation : null, false); String textLine = substring(field, (i == line) ? currentLocation : null, false);
SearchMatch match = matcher.apply(textLine); SearchMatch match = matcher.apply(textLine);
if (match != SearchMatch.NO_MATCH) { if (match == SearchMatch.NO_MATCH) {
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field); continue;
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, false, field.getText());
} }
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(), context);
} }
return null; return null;
} }
@ -317,7 +343,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
} }
String partialText = textField.getText(); String partialText = textField.getText();
if (forwardSearch) { if (forwardSearch) {
int nextCol = location.getCol(); 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

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,8 +20,7 @@ import java.awt.event.KeyEvent;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import docking.action.KeyBindingData; import docking.action.*;
import docking.action.MenuData;
import docking.widgets.FindDialog; import docking.widgets.FindDialog;
import ghidra.app.decompiler.component.DecompilerFindDialog; import ghidra.app.decompiler.component.DecompilerFindDialog;
import ghidra.app.decompiler.component.DecompilerPanel; import ghidra.app.decompiler.component.DecompilerPanel;
@ -40,6 +39,11 @@ public class FindAction extends AbstractDecompilerAction {
setEnabled(true); setEnabled(true);
} }
@Override
public KeyBindingType getKeyBindingType() {
return KeyBindingType.SHARED;
}
@Override @Override
public void dispose() { public void dispose() {
if (findDialog != null) { if (findDialog != null) {

View file

@ -22,6 +22,9 @@ color.bg.highlight = color.palette.lemonchiffon
color.bg.currentline = color.palette.aliceblue 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.valid = color.bg
color.bg.textfield.hint.invalid = color.palette.mistyrose color.bg.textfield.hint.invalid = color.palette.mistyrose
color.fg.textfield.hint = color.fg.messages.hint 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.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 color.bg.filechooser.shortcut = [color]system.color.bg.view

View file

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

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -101,7 +101,7 @@ public class DockingHelpBroker extends GHelpBroker {
@Override @Override
protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) { protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) {
helpModel.addHelpModelListener(helpModelListener); helpModel.addHelpModelListener(helpModelListener);
new HelpViewSearcher(jHelp, helpModel); new HelpViewSearcher(jHelp);
} }
@Override @Override

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,27 +18,21 @@ package docking.help;
import java.awt.Component; import java.awt.Component;
import java.awt.Window; import java.awt.Window;
import java.awt.event.*; import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.File; import java.io.File;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.Enumeration;
import java.util.regex.*;
import javax.help.*; import javax.help.*;
import javax.help.DefaultHelpModel.DefaultHighlight;
import javax.help.search.SearchEngine; import javax.help.search.SearchEngine;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import docking.DockingUtils; import docking.DockingUtils;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import docking.actions.KeyBindingUtils; import docking.actions.KeyBindingUtils;
import docking.widgets.*; import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.util.WindowUtilities; import generic.util.WindowUtilities;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.task.*;
/** /**
* Enables the Find Dialog for searching through the current page of a help document. * 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 = private static KeyStroke FIND_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F, DockingUtils.CONTROL_KEY_MODIFIER_MASK); 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 JHelp jHelp;
private SearchEngine searchEngine; private SearchEngine searchEngine;
private HelpModel helpModel;
private JEditorPane htmlEditorPane; private JEditorPane htmlEditorPane;
private FindDialog findDialog; private FindDialog findDialog;
private boolean startSearchFromBeginning; HelpViewSearcher(JHelp jHelp) {
private boolean settingHighlights;
HelpViewSearcher(JHelp jHelp, HelpModel helpModel) {
this.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(); grabSearchEngine();
JHelpContentViewer contentViewer = jHelp.getContentViewer(); JHelpContentViewer contentViewer = jHelp.getContentViewer();
contentViewer.addTextHelpModelListener(e -> {
if (settingHighlights) {
return; // ignore our changes
}
clearSearchState();
});
contentViewer.addHelpModelListener(e -> { contentViewer.addHelpModelListener(e -> {
URL url = e.getURL(); URL url = e.getURL();
@ -102,24 +66,22 @@ class HelpViewSearcher {
return; return;
} }
// currentPageURL = url;
String file = url.getFile(); String file = url.getFile();
int separatorIndex = file.lastIndexOf(File.separator); int separatorIndex = file.lastIndexOf(File.separator);
file = file.substring(separatorIndex + 1); file = file.substring(separatorIndex + 1);
findDialog.setTitle(DIALOG_TITLE_PREFIX + file); findDialog.setTitle(DIALOG_TITLE_PREFIX + file);
clearSearchState(); // new page
}); });
// note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration // note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration
htmlEditorPane = getHTMLEditorPane(contentViewer); htmlEditorPane = getHTMLEditorPane(contentViewer);
TextComponentSearcher searcher = new TextComponentSearcher(htmlEditorPane);
findDialog = new FindDialog(DIALOG_TITLE_PREFIX, searcher);
htmlEditorPane.addMouseListener(new MouseAdapter() { htmlEditorPane.addMouseListener(new MouseAdapter() {
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
htmlEditorPane.getCaret().setVisible(true); htmlEditorPane.getCaret().setVisible(true);
startSearchFromBeginning = false;
} }
}); });
@ -214,14 +176,6 @@ class HelpViewSearcher {
return (JEditorPane) viewport.getView(); return (JEditorPane) viewport.getView();
} }
private void clearSearchState() {
startSearchFromBeginning = true;
}
private void clearHighlights() {
((TextHelpModel) helpModel).removeAllHighlights();
}
//================================================================================================== //==================================================================================================
// Inner Classes // 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 { // private class IndexerSearchTask extends Task {
// //

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -27,10 +27,14 @@ import docking.ReusableDialogComponentProvider;
import docking.widgets.button.GRadioButton; import docking.widgets.button.GRadioButton;
import docking.widgets.combobox.GhidraComboBox; import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.label.GLabel; 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 { public class FindDialog extends ReusableDialogComponentProvider {
private GhidraComboBox<String> comboBox; protected GhidraComboBox<String> comboBox;
protected FindDialogSearcher searcher; protected FindDialogSearcher searcher;
private JButton nextButton; private JButton nextButton;
@ -38,6 +42,8 @@ public class FindDialog extends ReusableDialogComponentProvider {
private JRadioButton stringRadioButton; private JRadioButton stringRadioButton;
private JRadioButton regexRadioButton; private JRadioButton regexRadioButton;
private Callback closedCallback = Callback.dummy();
public FindDialog(String title, FindDialogSearcher searcher) { public FindDialog(String title, FindDialogSearcher searcher) {
super(title, false, true, true, true); super(title, false, true, true, true);
this.searcher = searcher; this.searcher = searcher;
@ -46,6 +52,16 @@ public class FindDialog extends ReusableDialogComponentProvider {
buildButtons(); buildButtons();
} }
@Override
public void dispose() {
searcher.dispose();
super.dispose();
}
public void setClosedCallback(Callback c) {
this.closedCallback = Callback.dummyIfNull(c);
}
private void buildButtons() { private void buildButtons() {
nextButton = new JButton("Next"); nextButton = new JButton("Next");
nextButton.setMnemonic('N'); nextButton.setMnemonic('N');
@ -113,7 +129,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
return mainPanel; return mainPanel;
} }
private void enableButtons(boolean b) { protected void enableButtons(boolean b) {
nextButton.setEnabled(b); nextButton.setEnabled(b);
previousButton.setEnabled(b); previousButton.setEnabled(b);
} }
@ -130,6 +146,8 @@ public class FindDialog extends ReusableDialogComponentProvider {
@Override @Override
protected void dialogClosed() { protected void dialogClosed() {
comboBox.setText(""); comboBox.setText("");
searcher.clearHighlights();
closedCallback.call();
} }
public void next() { public void next() {
@ -206,7 +224,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
// -don't allow searching again while notifying // -don't allow searching again while notifying
// -make sure the user can see it // -make sure the user can see it
enableButtons(false); enableButtons(false);
alertMessage(() -> enableButtons(true)); alertMessage(() -> {
String text = comboBox.getText();
enableButtons(text.length() != 0);
});
} }
@Override @Override
@ -214,6 +235,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
clearStatusText(); clearStatusText();
} }
public FindDialogSearcher getSearcher() {
return searcher;
}
String getText() { String getText() {
if (isVisible()) { if (isVisible()) {
return comboBox.getText(); return comboBox.getText();

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -60,6 +60,11 @@ public interface FindDialogSearcher {
*/ */
public void highlightSearchResults(SearchLocation location); 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 * Perform a search for the next item in the given direction starting at the given cursor
* position. * position.
@ -83,4 +88,11 @@ public interface FindDialogSearcher {
public default List<SearchLocation> searchAll(String text, boolean useRegex) { public default List<SearchLocation> searchAll(String text, boolean useRegex) {
throw new UnsupportedOperationException("Search All is not defined for this searcher"); 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 @Override
public void setUI(ComboBoxUI ui) { public void setUI(ComboBoxUI ui) {
int oldColumns = getColumns();
super.setUI(ui); 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 // 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 // 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 // in the init() method. We also reset the document if the client ever called the
// setDocument() method // setDocument() method.
installPassThroughListeners(); 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 * @param columnCount The number of columns for the text field editor
* @see JTextField#setColumns(int) * @see JTextField#setColumns(int)
* @deprecated use {@link #setColumns(int)}
*/ */
@Deprecated(forRemoval = true, since = "11.3")
public void setColumnCount(int columnCount) { public void setColumnCount(int columnCount) {
JTextField textField = getTextField(); setColumns(columnCount);
textField.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() * @see JTextField#selectAll()
*/ */
@ -297,16 +330,6 @@ public class GhidraComboBox<E> extends JComboBox<E> implements GComponent {
docListeners.remove(l); 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. * Convenience method for associating a label with the editor component.
* @param label the label to associate * @param label the label to associate

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -63,6 +63,11 @@ public class FindDialogTest {
// stub // stub
} }
@Override
public void clearHighlights() {
// stub
}
@Override @Override
public SearchLocation search(String text, CursorPosition cursorPosition, public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) { boolean searchForward, boolean useRegex) {

View file

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