diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index 7742023e5b..6cee9360df 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -24,7 +24,7 @@ color.fg.dialog.equates.equate = color.palette.blue color.fg.dialog.equates.suggestion = color.palette.hint color.fg.consoletextpane = color.fg -color.fg.error.consoletextpane = color.fg.error +color.fg.consoletextpane.error = color.fg.error color.fg.infopanel.version = color.fg @@ -34,8 +34,9 @@ color.fg.interpreterconsole.error = color.fg.error color.bg.markerservice = color.bg -color.bg.search.highlight = color.bg.highlight -color.bg.search.highlight.current.line = color.palette.yellow +color.bg.search.highlight = color.bg.find.highlight +color.bg.search.highlight.current.line = color.bg.find.highlight.active + color.fg.analysis.options.prototype = color.palette.crimson color.fg.analysis.options.prototype.selected = color.palette.lightcoral diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/console/ConsoleComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/console/ConsoleComponentProvider.java index 1565a705a1..5bec699d6b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/console/ConsoleComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/console/ConsoleComponentProvider.java @@ -4,9 +4,9 @@ * 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. @@ -20,11 +20,13 @@ import java.awt.event.*; import java.io.PrintWriter; import javax.swing.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; +import javax.swing.text.*; import docking.*; import docking.action.*; +import docking.action.builder.ActionBuilder; +import docking.widgets.FindDialog; +import docking.widgets.TextComponentSearcher; import generic.theme.GIcon; import generic.theme.Gui; import ghidra.app.services.*; @@ -53,13 +55,19 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement private ConsoleTextPane textPane; private JScrollPane scroller; private JComponent component; - private boolean scrollLock = false; - private DockingAction clearAction; - private ToggleDockingAction scrollAction; - private Address currentAddress; + private PrintWriter stderr; private PrintWriter stdin; + private boolean scrollLock = false; + + private DockingAction clearAction; + private ToggleDockingAction scrollAction; + private Program currentProgram; + private Address currentAddress; + + private FindDialog findDialog; + private TextComponentSearcher searcher; public ConsoleComponentProvider(PluginTool tool, String owner) { super(tool, "Console", owner); @@ -97,6 +105,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement textPane.dispose(); stderr.close(); stdin.close(); + + if (findDialog != null) { + findDialog.close(); + } } private void createOptions() { @@ -117,70 +129,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement textPane.setName(textPaneName); textPane.getAccessibleContext().setAccessibleName(textPaneName); - textPane.addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - if (currentProgram == null) { - return; - } - - Point hoverPoint = e.getPoint(); - ConsoleWord word = getWordSeparatedByWhitespace(hoverPoint); - if (word == null) { - textPane.setCursor(Cursor.getDefaultCursor()); - return; - } - - Address addr = currentProgram.getAddressFactory().getAddress(word.word); - if (addr != null || isSymbol(word.word)) { - textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - return; - } - - ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); - addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word); - if (addr != null || isSymbol(trimmedWord.word)) { - textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - return; - } - } - }); - textPane.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - if (e.getClickCount() != 2) { - return; - } - if (currentProgram == null) { - return; - } - - GoToService gotoService = tool.getService(GoToService.class); - if (gotoService == null) { - return; - } - - Point clickPoint = e.getPoint(); - ConsoleWord word = getWordSeparatedByWhitespace(clickPoint); - if (word == null) { - return; - } - - Address addr = currentProgram.getAddressFactory().getAddress(word.word); - if (addr != null || isSymbol(word.word)) { - goTo(word); - return; - } - - ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); - addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word); - if (addr == null && !isSymbol(trimmedWord.word)) { - return; - } - - goTo(trimmedWord); - } - }); + textPane.addMouseMotionListener(new CursorUpdateMouseMotionListener()); + textPane.addMouseListener(new GoToMouseListener()); scroller = new JScrollPane(textPane); scroller.setPreferredSize(new Dimension(200, 100)); @@ -191,74 +141,12 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement tool.addComponentProvider(this, true); } - private void goTo(ConsoleWord word) { - - GoToService gotoService = tool.getService(GoToService.class); - if (gotoService == null) { - return; - } - - // NOTE: must be case sensitive otherwise the service will report that it has - // processed the request even if there are no matches - boolean found = - gotoService.goToQuery(currentAddress, new QueryData(word.word, true), null, null); - if (found) { - select(word); - return; - } - - ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); - found = gotoService.goToQuery(currentAddress, new QueryData(trimmedWord.word, true), null, - null); - if (found) { - select(trimmedWord); - } - } - - private ConsoleWord getWordSeparatedByWhitespace(Point p) { - int pos = textPane.viewToModel2D(p); - Document doc = textPane.getDocument(); - int startIndex = pos; - int endIndex = pos; - try { - for (; startIndex > 0; --startIndex) { - char c = doc.getText(startIndex, 1).charAt(0); - if (Character.isWhitespace(c)) { - break; - } - } - for (; endIndex < doc.getLength() - 1; ++endIndex) { - char c = doc.getText(endIndex, 1).charAt(0); - if (Character.isWhitespace(c)) { - break; - } - } - String text = doc.getText(startIndex + 1, endIndex - startIndex); - if (text == null || text.trim().length() == 0) { - return null; - } - return new ConsoleWord(text.trim(), startIndex + 1, endIndex); - } - catch (BadLocationException ble) { - return null; - } - } - private boolean isSymbol(String word) { SymbolTable symbolTable = currentProgram.getSymbolTable(); SymbolIterator symbolIterator = symbolTable.getSymbols(word); return symbolIterator.hasNext(); } - protected void select(ConsoleWord word) { - try { - textPane.select(word.startPosition, word.endPosition); - } - catch (Exception e) { - // we are too lazy to verify our data before calling select--bleh - } - } - private void createActions() { clearAction = new DockingAction("Clear Console", getOwner()) { @@ -268,9 +156,7 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement } }; clearAction.setDescription("Clear Console"); -// ACTIONS - auto generated clearAction.setToolBarData(new ToolBarData(new GIcon("icon.plugin.console.clear"), null)); - clearAction.setEnabled(true); scrollAction = new ToggleDockingAction("Scroll Lock", getOwner()) { @@ -282,14 +168,33 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement scrollAction.setDescription("Scroll Lock"); scrollAction.setToolBarData( new ToolBarData(new GIcon("icon.plugin.console.scroll.lock"), null)); - scrollAction.setEnabled(true); scrollAction.setSelected(scrollLock); + //@formatter:off + new ActionBuilder("Find", getOwner()) + .keyBinding("Ctrl F") + .sharedKeyBinding() + .popupMenuPath("Find...") + .onAction(c -> { + showFindDialog(); + }) + .buildAndInstallLocal(this) + ; + //@formatter:on + addLocalAction(scrollAction); addLocalAction(clearAction); } + private void showFindDialog() { + if (findDialog == null) { + searcher = new TextComponentSearcher(textPane); + findDialog = new FindDialog("Find", searcher); + } + getTool().showDialog(findDialog); + } + @Override public void addMessage(String originator, String message) { checkVisible(); @@ -304,26 +209,6 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement @Override public void addException(String originator, Exception e) { - try { - e.printStackTrace(stderr); - } - catch (Exception e1) { - // - // sometimes an exception will occur while printing - // the stack trace on an exception. - // if that happens catch it and manually print - // some information about it. - // see org.jruby.exceptions.RaiseException - // - stderr.println("Unexpected Exception: " + e.getMessage()); - for (StackTraceElement stackTraceElement : e.getStackTrace()) { - stderr.println("\t" + stackTraceElement.toString()); - } - stderr.println("Unexpected Exception: " + e1.getMessage()); - for (StackTraceElement stackTraceElement : e1.getStackTrace()) { - stderr.println("\t" + stackTraceElement.toString()); - } - } Msg.error(this, "Unexpected Exception: " + e.getMessage(), e); } @@ -331,6 +216,10 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement public void clearMessages() { checkVisible(); textPane.setText(""); + + if (searcher != null) { + searcher.clearHighlights(); + } } @Override @@ -383,22 +272,21 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement return textPane.getDocument().getLength(); } - //////////////////////////////////////////////////////////////////// - private void checkVisible() { if (!isVisible()) { tool.showComponentProvider(this, true); } } - /** - * @see docking.ComponentProvider#getComponent() - */ @Override public JComponent getComponent() { return component; } + ConsoleTextPane getTextPane() { + return textPane; + } + public void setCurrentProgram(Program program) { currentProgram = program; } @@ -407,4 +295,136 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement currentAddress = address; } + private static ConsoleWord getWordSeparatedByWhitespace(JTextComponent textComponent, Point p) { + int pos = textComponent.viewToModel2D(p); + Document doc = textComponent.getDocument(); + int startIndex = pos; + int endIndex = pos; + try { + for (; startIndex > 0; --startIndex) { + char c = doc.getText(startIndex, 1).charAt(0); + if (Character.isWhitespace(c)) { + break; + } + } + for (; endIndex < doc.getLength() - 1; ++endIndex) { + char c = doc.getText(endIndex, 1).charAt(0); + if (Character.isWhitespace(c)) { + break; + } + } + String text = doc.getText(startIndex + 1, endIndex - startIndex); + if (text == null || text.trim().length() == 0) { + return null; + } + return new ConsoleWord(text.trim(), startIndex + 1, endIndex); + } + catch (BadLocationException ble) { + return null; + } + } + +//================================================================================================= +// Inner Classes +//================================================================================================= + + private class GoToMouseListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + if (e.getClickCount() != 2) { + return; + } + if (currentProgram == null) { + return; + } + + GoToService gotoService = tool.getService(GoToService.class); + if (gotoService == null) { + return; + } + + Point clickPoint = e.getPoint(); + ConsoleWord word = getWordSeparatedByWhitespace(textPane, clickPoint); + if (word == null) { + return; + } + + Address addr = currentProgram.getAddressFactory().getAddress(word.word); + if (addr != null || isSymbol(word.word)) { + goTo(word); + return; + } + + ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); + addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word); + if (addr == null && !isSymbol(trimmedWord.word)) { + return; + } + + goTo(trimmedWord); + } + + private void goTo(ConsoleWord word) { + + GoToService gotoService = tool.getService(GoToService.class); + if (gotoService == null) { + return; + } + + // NOTE: must be case sensitive otherwise the service will report that it has + // processed the request even if there are no matches + boolean found = + gotoService.goToQuery(currentAddress, new QueryData(word.word, true), null, null); + if (found) { + select(word); + return; + } + + ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); + found = + gotoService.goToQuery(currentAddress, new QueryData(trimmedWord.word, true), null, + null); + if (found) { + select(trimmedWord); + } + } + + private void select(ConsoleWord word) { + try { + textPane.select(word.startPosition, word.endPosition); + } + catch (Exception e) { + // we are too lazy to verify our data before calling select--bleh + } + } + } + + private class CursorUpdateMouseMotionListener extends MouseMotionAdapter { + @Override + public void mouseMoved(MouseEvent e) { + if (currentProgram == null) { + return; + } + + Point hoverPoint = e.getPoint(); + ConsoleWord word = getWordSeparatedByWhitespace(textPane, hoverPoint); + if (word == null) { + textPane.setCursor(Cursor.getDefaultCursor()); + return; + } + + Address addr = currentProgram.getAddressFactory().getAddress(word.word); + if (addr != null || isSymbol(word.word)) { + textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return; + } + + ConsoleWord trimmedWord = word.getWordWithoutSpecialCharacters(); + addr = currentProgram.getAddressFactory().getAddress(trimmedWord.word); + if (addr != null || isSymbol(trimmedWord.word)) { + textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return; + } + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/flowarrow/FlowArrowPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/flowarrow/FlowArrowPanel.java index a929669206..9214a58805 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/flowarrow/FlowArrowPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/flowarrow/FlowArrowPanel.java @@ -4,9 +4,9 @@ * 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. @@ -45,7 +45,6 @@ class FlowArrowPanel extends JPanel { private Point pendingMouseClickPoint; FlowArrowPanel(FlowArrowPlugin p) { - super(); this.plugin = p; setMinimumSize(new Dimension(0, 0)); setPreferredSize(new Dimension(32, 1)); @@ -164,7 +163,6 @@ class FlowArrowPanel extends JPanel { ScrollingCallback callback = new ScrollingCallback(start, end); Animator animator = AnimationUtils.executeSwingAnimationCallback(callback); callback.setAnimator(animator); - } private void processSingleClick(Point point) { @@ -322,7 +320,9 @@ class FlowArrowPanel extends JPanel { if (current.equals(end)) { // we are done! - animator.stop(); + if (animator != null) { + animator.stop(); + } return; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterComponentProvider.java index ebf7ddc7f4..a5fd7ab553 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterComponentProvider.java @@ -19,13 +19,13 @@ import java.io.*; import java.util.ArrayList; import java.util.List; -import javax.swing.Icon; -import javax.swing.JComponent; +import javax.swing.*; import docking.ActionContext; import docking.action.DockingAction; import docking.action.ToolBarData; -import docking.widgets.OptionDialog; +import docking.action.builder.ActionBuilder; +import docking.widgets.*; import generic.theme.GIcon; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.util.HelpLocation; @@ -39,6 +39,9 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter private InterpreterConnection interpreter; private List firstActivationCallbacks; + private FindDialog findDialog; + private TextComponentSearcher searcher; + public InterpreterComponentProvider(InterpreterPanelPlugin plugin, InterpreterConnection interpreter, boolean visible) { super(plugin.getTool(), interpreter.getTitle(), interpreter.getTitle()); @@ -72,8 +75,28 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter clearAction.setDescription("Clear Interpreter"); clearAction.setToolBarData(new ToolBarData(Icons.CLEAR_ICON, null)); clearAction.setEnabled(true); - addLocalAction(clearAction); + + //@formatter:off + new ActionBuilder("Find", getOwner()) + .keyBinding("Ctrl F") + .sharedKeyBinding() + .popupMenuPath("Find...") + .onAction(c -> { + showFindDialog(); + }) + .buildAndInstallLocal(this) + ; + //@formatter:on + } + + private void showFindDialog() { + if (findDialog == null) { + JTextPane textPane = panel.getOutputTextPane(); + searcher = new TextComponentSearcher(textPane); + findDialog = new FindDialog("Find", searcher); + } + getTool().showDialog(findDialog); } @Override @@ -128,6 +151,10 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter @Override public void clear() { panel.clear(); + + if (searcher != null) { + searcher.clearHighlights(); + } } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterPanel.java index 48b73d53f3..9a2f8cf60e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/InterpreterPanel.java @@ -4,9 +4,9 @@ * 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. @@ -77,7 +77,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener { private SimpleAttributeSet STDIN_SET; private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB; - private boolean highlightCompletion = false; private int completionInsertionPosition; private boolean caretGuard = true; @@ -298,12 +297,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener { completionWindowTrigger = options.getEnum(COMPLETION_WINDOW_TRIGGER_LABEL, CompletionWindowTrigger.TAB); -// TODO -// highlightCompletion = -// options.getBoolean(HIGHLIGHT_COMPLETION_OPTION_LABEL, DEFAULT_HIGHLIGHT_COMPLETION); -// options.setDescription(HIGHLIGHT_COMPLETION_OPTION_LABEL, HIGHLIGHT_COMPLETION_DESCRIPTION); -// options.addOptionsChangeListener(this); - options.addOptionsChangeListener(this); } @@ -317,10 +310,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener { else if (optionName.equals(COMPLETION_WINDOW_TRIGGER_LABEL)) { completionWindowTrigger = (CompletionWindowTrigger) newValue; } -// TODO -// else if (optionName.equals(HIGHLIGHT_COMPLETION_OPTION_LABEL)) { -// highlightCompletion = ((Boolean) newValue).booleanValue(); -// } } @Override @@ -480,6 +469,10 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener { stdin.resetStream(); } + public JTextPane getOutputTextPane() { + return outputTextPane; + } + public String getOutputText() { return outputTextPane.getText(); } @@ -535,16 +528,8 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener { text.substring(0, insertedTextStart) + insertion + text.substring(position); setInputTextPaneText(inputText); - /* Select what we inserted so that the user can easily - * get rid of what they did (in case of a mistake). */ - if (highlightCompletion) { - inputTextPane.setSelectionStart(insertedTextStart); - inputTextPane.moveCaretPosition(insertedTextEnd); - } - else { - /* Then put the caret right after what we inserted. */ - inputTextPane.setCaretPosition(insertedTextEnd); - } + /* Then put the caret right after what we inserted. */ + inputTextPane.setCaretPosition(insertedTextEnd); updateCompletionList(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java index befd258926..f32686bdfd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/AddEditDialog.java @@ -4,9 +4,9 @@ * 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. @@ -560,7 +560,7 @@ public class AddEditDialog extends ReusableDialogComponentProvider { } }; // the number of columns determines the default width of the add/edit label dialog - labelNameChoices.setColumnCount(20); + labelNameChoices.setColumns(20); labelNameChoices.setName("label.name.choices"); GhidraComboBox comboBox = new GhidraComboBox<>(); comboBox.setEnterKeyForwarding(true); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ConsoleTextPane.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ConsoleTextPane.java index 0d7a51e43c..0898a3b0bd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ConsoleTextPane.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ConsoleTextPane.java @@ -4,9 +4,9 @@ * 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. @@ -32,8 +32,6 @@ import ghidra.util.task.SwingUpdateManager; /** * A generic text pane that is used as a console to which text can be written. - * - * There is not test for this class, but it is indirectly tested by FrontEndGuiTest. */ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener { @@ -211,7 +209,7 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener outputAttributes = new GAttributes(font, new GColor("color.fg.consoletextpane")); outputAttributes.addAttribute(CUSTOM_ATTRIBUTE_KEY, OUTPUT_ATTRIBUTE_VALUE); - errorAttributes = new GAttributes(font, new GColor("color.fg.error.consoletextpane")); + errorAttributes = new GAttributes(font, new GColor("color.fg.consoletextpane.error")); errorAttributes.addAttribute(CUSTOM_ATTRIBUTE_KEY, ERROR_ATTRIBUTE_VALUE); } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java new file mode 100644 index 0000000000..6a6e0f64bf --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java @@ -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 matches = getMatches(); + assertEquals(4, matches.size()); + verfyHighlightColor(matches); + + close(findDialog); + verifyDefaultBackgroundColorForAllText(); + } + + @Test + public void testFindHighlights_ChangeSearchText() throws Exception { + + List 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 matches = getMatches(); + assertEquals(4, matches.size()); + verfyHighlightColor(matches); + + runSwing(() -> textPane.setText("This is some\nnew text.")); + + verifyDefaultBackgroundColorForAllText(); + assertSearchModelHasStaleSearchResults(); + } + + @Test + public void testMovingCursorUpdatesActiveHighlight() { + + List 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 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 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 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 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 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 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 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 getMatches() { + + String searchText = findDialog.getSearchText(); + List 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 + ']'; + } + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/ConsoleTextPaneTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/ConsoleTextPaneTest.java index f69f32fc5c..d4f7900b4a 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/ConsoleTextPaneTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/ConsoleTextPaneTest.java @@ -4,9 +4,9 @@ * 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. @@ -21,14 +21,13 @@ import java.util.function.Supplier; import javax.swing.JFrame; import javax.swing.JScrollPane; -import javax.swing.text.Document; import org.junit.Test; import generic.test.AbstractGuiTest; import ghidra.framework.plugintool.DummyPluginTool; -public class ConsoleTextPaneTest { +public class ConsoleTextPaneTest extends AbstractGuiTest { private int runNumber = 1; @@ -104,34 +103,36 @@ public class ConsoleTextPaneTest { assertCaretAtBottom(text); } +//================================================================================================= +// Private Methods +//================================================================================================= + private void setCaret(ConsoleTextPane text, int position) { swing(() -> text.setCaretPosition(position)); } private void assertCaretAtTop(ConsoleTextPane text) { - AbstractGuiTest.waitForSwing(); + waitForSwing(); int expectedPosition = 0; assertCaretPosition(text, expectedPosition); } private void assertCaretAtBottom(ConsoleTextPane text) { - AbstractGuiTest.waitForSwing(); + waitForSwing(); int expectedPosition = text.getDocument().getLength(); assertCaretPosition(text, expectedPosition); } private void assertCaretPosition(ConsoleTextPane text, int expectedPosition) { - - AbstractGuiTest.waitForSwing(); - Document doc = text.getDocument(); + waitForSwing(); int actualPosition = swing(() -> text.getCaretPosition()); assertEquals(expectedPosition, actualPosition); } private void printEnoughLinesToOverflowTheMaxCharCount(ConsoleTextPane text) { - AbstractGuiTest.runSwing(() -> { + runSwing(() -> { int charsWritten = 0; for (int i = 0; charsWritten < text.getMaximumCharacterLimit(); i++) { @@ -145,10 +146,10 @@ public class ConsoleTextPaneTest { } private void swing(Runnable r) { - AbstractGuiTest.runSwing(r); + runSwing(r); } private T swing(Supplier s) { - return AbstractGuiTest.runSwing(s); + return runSwing(s); } } diff --git a/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java b/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java index 030b0c34aa..6a3630a8dd 100644 --- a/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java +++ b/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java @@ -4,9 +4,9 @@ * 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. @@ -35,11 +35,11 @@ import ghidra.util.datastruct.Duo.Side; public class DecompilerDiffViewFindAction extends DockingAction { - private Duo findDialogs; + private Duo findDialogs = new Duo<>(); private PluginTool tool; public DecompilerDiffViewFindAction(String owner, PluginTool tool) { - super("Find", owner, true); + super("Find", owner, KeyBindingType.SHARED); setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind")); setPopupMenuData(new MenuData(new String[] { "Find..." }, "Decompile")); setKeyBindingData( @@ -48,6 +48,12 @@ public class DecompilerDiffViewFindAction extends DockingAction { this.tool = tool; } + @Override + public void dispose() { + super.dispose(); + findDialogs.each(dialog -> dialog.dispose()); + } + @Override public boolean isAddToPopup(ActionContext context) { return (context instanceof DualDecompilerActionContext); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java index 5d0eb9efb2..52497d160e 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java @@ -15,6 +15,7 @@ */ package ghidra.app.decompiler.component; +import java.awt.Component; import java.util.List; import java.util.stream.Collectors; @@ -26,10 +27,10 @@ import docking.widgets.FindDialog; import docking.widgets.SearchLocation; import docking.widgets.button.GButton; import docking.widgets.fieldpanel.support.FieldLocation; -import docking.widgets.table.AbstractDynamicTableColumnStub; -import docking.widgets.table.TableColumnDescriptor; +import docking.widgets.table.*; import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation; import ghidra.app.plugin.core.decompile.actions.DecompilerSearcher; +import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext; import ghidra.app.plugin.core.table.TableComponentProvider; import ghidra.app.util.HelpTopics; import ghidra.app.util.query.TableService; @@ -43,11 +44,14 @@ import ghidra.util.Msg; import ghidra.util.datastruct.Accumulator; import ghidra.util.exception.CancelledException; import ghidra.util.table.*; +import ghidra.util.table.column.AbstractGhidraColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; import ghidra.util.task.TaskMonitor; public class DecompilerFindDialog extends FindDialog { private DecompilerPanel decompilerPanel; + private GButton showAllButton; public DecompilerFindDialog(DecompilerPanel decompilerPanel) { super("Decompiler Find Text", new DecompilerSearcher(decompilerPanel)); @@ -55,7 +59,7 @@ public class DecompilerFindDialog extends FindDialog { setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind")); - GButton showAllButton = new GButton("Search All"); + showAllButton = new GButton("Search All"); showAllButton.addActionListener(e -> showAll()); // move this button to the end @@ -65,7 +69,16 @@ public class DecompilerFindDialog extends FindDialog { addButton(dismissButton); } + @Override + protected void enableButtons(boolean b) { + super.enableButtons(b); + showAllButton.setEnabled(b); + } + private void showAll() { + + String searchText = getSearchText(); + close(); DockingWindowManager dwm = DockingWindowManager.getActiveInstance(); @@ -78,7 +91,7 @@ public class DecompilerFindDialog extends FindDialog { return; } - List results = searcher.searchAll(getSearchText(), useRegex()); + List results = searcher.searchAll(searchText, useRegex()); if (!results.isEmpty()) { // save off searches that find results so users can reuse them later storeSearchText(getSearchText()); @@ -127,12 +140,6 @@ public class DecompilerFindDialog extends FindDialog { provider.setTabText("'%s'".formatted(getSearchText())); } - @Override - protected void dialogClosed() { - // clear the search results when the dialog is closed - decompilerPanel.setSearchResults(null); - } - //================================================================================================= // Inner Classes //================================================================================================= @@ -202,18 +209,57 @@ public class DecompilerFindDialog extends FindDialog { } private class ContextColumn - extends AbstractDynamicTableColumnStub { + extends + AbstractDynamicTableColumnStub { + + private GColumnRenderer renderer = new ContextCellRenderer(); @Override - public String getValue(DecompilerSearchLocation rowObject, Settings settings, + public LocationReferenceContext getValue(DecompilerSearchLocation rowObject, + Settings settings, ServiceProvider sp) throws IllegalArgumentException { - return rowObject.getTextLine(); + + LocationReferenceContext context = rowObject.getContext(); + return context; + // return rowObject.getTextLine(); } @Override public String getColumnName() { return "Context"; } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + private class ContextCellRenderer + extends AbstractGhidraColumnRenderer { + + { + // 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(); + } + } } } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java index 1d0e809af1..600835d6c4 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java @@ -4,9 +4,9 @@ * 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. @@ -18,18 +18,22 @@ package ghidra.app.plugin.core.decompile.actions; import docking.widgets.CursorPosition; import docking.widgets.SearchLocation; import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext; public class DecompilerSearchLocation extends SearchLocation { private final FieldLocation fieldLocation; private String textLine; + private LocationReferenceContext context; public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive, - int endIndexInclusive, String searchText, boolean forwardDirection, String textLine) { + int endIndexInclusive, String searchText, boolean forwardDirection, String textLine, + LocationReferenceContext context) { super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection); this.fieldLocation = fieldLocation; this.textLine = textLine; + this.context = context; } public FieldLocation getFieldLocation() { @@ -40,6 +44,10 @@ public class DecompilerSearchLocation extends SearchLocation { return textLine; } + public LocationReferenceContext getContext() { + return context; + } + @Override public CursorPosition getCursorPosition() { return new DecompilerCursorPosition(fieldLocation); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java index d47ef4201b..ba6dcbdcb1 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java @@ -4,9 +4,9 @@ * 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. @@ -25,6 +25,8 @@ import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.fieldpanel.support.RowColLocation; import ghidra.app.decompiler.component.ClangTextField; import ghidra.app.decompiler.component.DecompilerPanel; +import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext; +import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder; import ghidra.util.Msg; import ghidra.util.UserSearchUtils; @@ -84,6 +86,16 @@ public class DecompilerSearcher implements FindDialogSearcher { decompilerPanel.setSearchResults(location); } + @Override + public void clearHighlights() { + decompilerPanel.setSearchResults(null); + } + + @Override + public void dispose() { + clearHighlights(); + } + @Override public SearchLocation search(String text, CursorPosition position, boolean searchForward, boolean useRegex) { @@ -160,7 +172,6 @@ public class DecompilerSearcher implements FindDialogSearcher { results.add(searchLocation); FieldLocation last = searchLocation.getFieldLocation(); - int line = last.getIndex().intValue(); int field = 0; // there is only 1 field int row = 0; // there is only 1 row @@ -260,14 +271,15 @@ public class DecompilerSearcher implements FindDialogSearcher { if (match == SearchMatch.NO_MATCH) { continue; } + + String fullLine = field.getText(); if (i == line) { // cursor is on this line // // The match start for all lines without the cursor will be relative to the start // 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 // compensate for the difference between the start of the line and the cursor. - // - String fullLine = field.getText(); + // int cursorOffset = fullLine.length() - partialLine.length(); match.start += cursorOffset; match.end += cursorOffset; @@ -276,13 +288,26 @@ public class DecompilerSearcher implements FindDialogSearcher { FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field); FieldLocation fieldLocation = new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column()); - + LocationReferenceContext context = createContext(fullLine, match); return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1, - searchString, true, field.getText()); + searchString, true, field.getText(), context); } return null; } + private LocationReferenceContext createContext(String line, SearchMatch match) { + LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder(); + int start = match.start; + int end = match.end; + builder.append(line.substring(0, start)); + builder.appendMatch(line.substring(start, end)); + if (end < line.length()) { + builder.append(line.substring(end)); + } + + return builder.build(); + } + private DecompilerSearchLocation findPrevious(Function matcher, String searchString, FieldLocation currentLocation) { @@ -291,16 +316,17 @@ public class DecompilerSearcher implements FindDialogSearcher { for (int i = line; i >= 0; i--) { ClangTextField field = (ClangTextField) fields.get(i); String textLine = substring(field, (i == line) ? currentLocation : null, false); - SearchMatch match = matcher.apply(textLine); - if (match != SearchMatch.NO_MATCH) { - FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field); - FieldLocation fieldLocation = - new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column()); - - return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1, - searchString, false, field.getText()); + if (match == SearchMatch.NO_MATCH) { + continue; } + + FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field); + FieldLocation fieldLocation = + new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column()); + LocationReferenceContext context = createContext(field.getText(), match); + return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1, + searchString, false, field.getText(), context); } return null; } @@ -317,7 +343,6 @@ public class DecompilerSearcher implements FindDialogSearcher { } String partialText = textField.getText(); - if (forwardSearch) { int nextCol = location.getCol(); @@ -365,6 +390,5 @@ public class DecompilerSearcher implements FindDialogSearcher { } } - private record FieldLineLocation(int fieldNumber, int column) { - } + private record FieldLineLocation(int fieldNumber, int column) {} } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java index e8e6e182a0..3b72a457f6 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java @@ -4,9 +4,9 @@ * 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. @@ -20,8 +20,7 @@ import java.awt.event.KeyEvent; import org.apache.commons.lang3.StringUtils; -import docking.action.KeyBindingData; -import docking.action.MenuData; +import docking.action.*; import docking.widgets.FindDialog; import ghidra.app.decompiler.component.DecompilerFindDialog; import ghidra.app.decompiler.component.DecompilerPanel; @@ -40,6 +39,11 @@ public class FindAction extends AbstractDecompilerAction { setEnabled(true); } + @Override + public KeyBindingType getKeyBindingType() { + return KeyBindingType.SHARED; + } + @Override public void dispose() { if (findDialog != null) { diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index 662e828b60..3b8775690a 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -22,6 +22,9 @@ color.bg.highlight = color.palette.lemonchiffon color.bg.currentline = color.palette.aliceblue +color.bg.find.highlight = color.palette.yellow +color.bg.find.highlight.active = color.palette.orange + color.bg.textfield.hint.valid = color.bg color.bg.textfield.hint.invalid = color.palette.mistyrose color.fg.textfield.hint = color.fg.messages.hint @@ -184,7 +187,8 @@ font.wizard.border.title = sansserif-plain-10 color.fg.filterfield = color.palette.darkslategray -color.bg.highlight = #703401 // orangish +color.bg.highlight = #67582A // olivish +color.bg.find.highlight.active = #A24E05 // orangish color.bg.filechooser.shortcut = [color]system.color.bg.view diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index a9edf70295..013d4100ed 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -725,6 +725,12 @@ public class DialogComponentProvider return; } + Callback animatorFinishedCallback = () -> { + statusLabel.setVisible(true); + alertFinishedCallback.call(); + isAlerting = false; + }; + isAlerting = true; // Note: manually call validate() so the 'statusLabel' updates its bounds after @@ -733,14 +739,17 @@ public class DialogComponentProvider mainPanel.validate(); statusLabel.setVisible(false); // disable painting in this dialog so we don't see double Animator animator = AnimationUtils.pulseComponent(statusLabel, 1); - animator.addTarget(new TimingTargetAdapter() { - @Override - public void end() { - statusLabel.setVisible(true); - alertFinishedCallback.call(); - isAlerting = false; - } - }); + if (animator == null) { + animatorFinishedCallback.call(); + } + else { + animator.addTarget(new TimingTargetAdapter() { + @Override + public void end() { + animatorFinishedCallback.call(); + } + }); + } } protected Color getStatusColor(MessageType type) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/DockingHelpBroker.java b/Ghidra/Framework/Docking/src/main/java/docking/help/DockingHelpBroker.java index 6298594202..6f8f54afa5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/DockingHelpBroker.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/DockingHelpBroker.java @@ -4,9 +4,9 @@ * 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. @@ -101,7 +101,7 @@ public class DockingHelpBroker extends GHelpBroker { @Override protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) { helpModel.addHelpModelListener(helpModelListener); - new HelpViewSearcher(jHelp, helpModel); + new HelpViewSearcher(jHelp); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpViewSearcher.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpViewSearcher.java index 33045e9ff8..c8c2b11d9a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpViewSearcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpViewSearcher.java @@ -4,9 +4,9 @@ * 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. @@ -18,27 +18,21 @@ package docking.help; import java.awt.Component; import java.awt.Window; import java.awt.event.*; -import java.awt.geom.Rectangle2D; import java.io.File; import java.net.URL; -import java.util.*; -import java.util.regex.*; +import java.util.Enumeration; import javax.help.*; -import javax.help.DefaultHelpModel.DefaultHighlight; import javax.help.search.SearchEngine; import javax.swing.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; import docking.DockingUtils; import docking.DockingWindowManager; import docking.actions.KeyBindingUtils; -import docking.widgets.*; +import docking.widgets.FindDialog; +import docking.widgets.TextComponentSearcher; import generic.util.WindowUtilities; -import ghidra.util.Msg; import ghidra.util.exception.AssertException; -import ghidra.util.task.*; /** * Enables the Find Dialog for searching through the current page of a help document. @@ -51,49 +45,19 @@ class HelpViewSearcher { private static KeyStroke FIND_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_F, DockingUtils.CONTROL_KEY_MODIFIER_MASK); - private Comparator searchResultComparator = - (o1, o2) -> o1.getBegin() - o2.getBegin(); - - private Comparator searchResultReverseComparator = - (o1, o2) -> o2.getBegin() - o1.getBegin(); - private JHelp jHelp; private SearchEngine searchEngine; - private HelpModel helpModel; private JEditorPane htmlEditorPane; private FindDialog findDialog; - private boolean startSearchFromBeginning; - private boolean settingHighlights; - - HelpViewSearcher(JHelp jHelp, HelpModel helpModel) { + HelpViewSearcher(JHelp jHelp) { this.jHelp = jHelp; - this.helpModel = helpModel; - - findDialog = new FindDialog(DIALOG_TITLE_PREFIX, new Searcher()) { - @Override - public void close() { - super.close(); - clearHighlights(); - } - }; - -// URL startURL = helpModel.getCurrentURL(); -// if (isValidHelpURL(startURL)) { -// currentPageURL = startURL; -// } grabSearchEngine(); JHelpContentViewer contentViewer = jHelp.getContentViewer(); - contentViewer.addTextHelpModelListener(e -> { - if (settingHighlights) { - return; // ignore our changes - } - clearSearchState(); - }); contentViewer.addHelpModelListener(e -> { URL url = e.getURL(); @@ -102,24 +66,22 @@ class HelpViewSearcher { return; } -// currentPageURL = url; - String file = url.getFile(); int separatorIndex = file.lastIndexOf(File.separator); file = file.substring(separatorIndex + 1); findDialog.setTitle(DIALOG_TITLE_PREFIX + file); - - clearSearchState(); // new page }); // note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration htmlEditorPane = getHTMLEditorPane(contentViewer); + TextComponentSearcher searcher = new TextComponentSearcher(htmlEditorPane); + findDialog = new FindDialog(DIALOG_TITLE_PREFIX, searcher); + htmlEditorPane.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { htmlEditorPane.getCaret().setVisible(true); - startSearchFromBeginning = false; } }); @@ -214,14 +176,6 @@ class HelpViewSearcher { return (JEditorPane) viewport.getView(); } - private void clearSearchState() { - startSearchFromBeginning = true; - } - - private void clearHighlights() { - ((TextHelpModel) helpModel).removeAllHighlights(); - } - //================================================================================================== // Inner Classes //================================================================================================== @@ -239,156 +193,6 @@ class HelpViewSearcher { } } - private class Searcher implements FindDialogSearcher { - - @Override - public CursorPosition getCursorPosition() { - if (startSearchFromBeginning) { - startSearchFromBeginning = false; - return new CursorPosition(0); - } - - int caretPosition = htmlEditorPane.getCaretPosition(); - return new CursorPosition(caretPosition); - } - - @Override - public CursorPosition getStart() { - return new CursorPosition(0); - } - - @Override - public CursorPosition getEnd() { - int length = htmlEditorPane.getDocument().getLength(); - return new CursorPosition(length - 1); - } - - @Override - public void setCursorPosition(CursorPosition position) { - int cursorPosition = position.getPosition(); - htmlEditorPane.setCaretPosition(cursorPosition); - } - - @Override - public void highlightSearchResults(SearchLocation location) { - if (location == null) { - ((TextHelpModel) helpModel).setHighlights(new DefaultHighlight[0]); - return; - } - - int start = location.getStartIndexInclusive(); - DefaultHighlight[] h = new DefaultHighlight[] { - new DefaultHighlight(start, location.getEndIndexInclusive()) }; - - // using setHighlights() instead of removeAll + add - // avoids one highlighting event - try { - settingHighlights = true; - ((TextHelpModel) helpModel).setHighlights(h); - htmlEditorPane.getCaret().setVisible(true); // bug - } - finally { - settingHighlights = false; - } - - try { - Rectangle2D rectangle = htmlEditorPane.modelToView2D(start); - htmlEditorPane.scrollRectToVisible(rectangle.getBounds()); - } - catch (BadLocationException e) { - // shouldn't happen - } - } - - @Override - public SearchLocation search(String text, CursorPosition cursorPosition, - boolean searchForward, boolean useRegex) { - ScreenSearchTask searchTask = new ScreenSearchTask(text, useRegex); - new TaskLauncher(searchTask, htmlEditorPane); - - List 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 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 getSearchResults() { - return searchHits; - } - } // // private class IndexerSearchTask extends Task { // diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java index c19f10d8ab..d692b4e2a0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java @@ -4,9 +4,9 @@ * 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. @@ -27,10 +27,14 @@ import docking.ReusableDialogComponentProvider; import docking.widgets.button.GRadioButton; import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GLabel; +import utility.function.Callback; +/** + * A dialog used to perform text searches on a text display. + */ public class FindDialog extends ReusableDialogComponentProvider { - private GhidraComboBox comboBox; + protected GhidraComboBox comboBox; protected FindDialogSearcher searcher; private JButton nextButton; @@ -38,6 +42,8 @@ public class FindDialog extends ReusableDialogComponentProvider { private JRadioButton stringRadioButton; private JRadioButton regexRadioButton; + private Callback closedCallback = Callback.dummy(); + public FindDialog(String title, FindDialogSearcher searcher) { super(title, false, true, true, true); this.searcher = searcher; @@ -46,6 +52,16 @@ public class FindDialog extends ReusableDialogComponentProvider { buildButtons(); } + @Override + public void dispose() { + searcher.dispose(); + super.dispose(); + } + + public void setClosedCallback(Callback c) { + this.closedCallback = Callback.dummyIfNull(c); + } + private void buildButtons() { nextButton = new JButton("Next"); nextButton.setMnemonic('N'); @@ -113,7 +129,7 @@ public class FindDialog extends ReusableDialogComponentProvider { return mainPanel; } - private void enableButtons(boolean b) { + protected void enableButtons(boolean b) { nextButton.setEnabled(b); previousButton.setEnabled(b); } @@ -130,6 +146,8 @@ public class FindDialog extends ReusableDialogComponentProvider { @Override protected void dialogClosed() { comboBox.setText(""); + searcher.clearHighlights(); + closedCallback.call(); } public void next() { @@ -206,7 +224,10 @@ public class FindDialog extends ReusableDialogComponentProvider { // -don't allow searching again while notifying // -make sure the user can see it enableButtons(false); - alertMessage(() -> enableButtons(true)); + alertMessage(() -> { + String text = comboBox.getText(); + enableButtons(text.length() != 0); + }); } @Override @@ -214,6 +235,10 @@ public class FindDialog extends ReusableDialogComponentProvider { clearStatusText(); } + public FindDialogSearcher getSearcher() { + return searcher; + } + String getText() { if (isVisible()) { return comboBox.getText(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogSearcher.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogSearcher.java index 427e6b835b..9b6971975c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogSearcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogSearcher.java @@ -4,9 +4,9 @@ * 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. @@ -60,6 +60,11 @@ public interface FindDialogSearcher { */ public void highlightSearchResults(SearchLocation location); + /** + * Clears any active highlights. + */ + public void clearHighlights(); + /** * Perform a search for the next item in the given direction starting at the given cursor * position. @@ -83,4 +88,11 @@ public interface FindDialogSearcher { public default List searchAll(String text, boolean useRegex) { throw new UnsupportedOperationException("Search All is not defined for this searcher"); } + + /** + * Disposes this searcher. This does nothing by default. + */ + public default void dispose() { + // stub + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/TextComponentSearcher.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/TextComponentSearcher.java new file mode 100644 index 0000000000..58dc2641d9 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/TextComponentSearcher.java @@ -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 matchesByPosition; + private FindMatch activeMatch; + private boolean isStale; + private String searchText; + + SearchResults(String searchText, TreeMap 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 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 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 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 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(); + } + + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java index c7d995207e..231d2f9f9b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java @@ -108,12 +108,16 @@ public class GhidraComboBox extends JComboBox implements GComponent { @Override public void setUI(ComboBoxUI ui) { + + int oldColumns = getColumns(); + super.setUI(ui); - // this gets called during construction and during theming changes. It always + + // This gets called during construction and during theming changes. It always // creates a new editor and any listeners or documents set on the current editor are // lost. So to combat this, we install the pass through listeners here instead of // in the init() method. We also reset the document if the client ever called the - // setDocument() method + // setDocument() method. installPassThroughListeners(); @@ -134,6 +138,13 @@ public class GhidraComboBox extends JComboBox 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 extends JComboBox implements GComponent { * * @param columnCount The number of columns for the text field editor * @see JTextField#setColumns(int) + * @deprecated use {@link #setColumns(int)} */ + @Deprecated(forRemoval = true, since = "11.3") public void setColumnCount(int columnCount) { - JTextField textField = getTextField(); - textField.setColumns(columnCount); + setColumns(columnCount); } /** - * Selects the text in the text field editor usd by this combo box. + * Sets the number of column's in the editor's component (JTextField). + * @param columns the number of columns to show + * @see JTextField#setColumns(int) + */ + public void setColumns(int columns) { + JTextField textField = getTextField(); + textField.setColumns(columns); + } + + private int getColumns() { + ComboBoxEditor currentEditor = getEditor(); + if (currentEditor != null) { + Object object = currentEditor.getEditorComponent(); + if (object instanceof JTextField textField) { + return textField.getColumns(); + } + } + return -1; + } + + /** + * Selects the text in the text field editor used by this combo box. * * @see JTextField#selectAll() */ @@ -297,16 +330,6 @@ public class GhidraComboBox extends JComboBox implements GComponent { docListeners.remove(l); } - /** - * Sets the number of column's in the editor's component (JTextField). - * @param columns the number of columns to show - * @see JTextField#setColumns(int) - */ - public void setColumns(int columns) { - JTextField textField = getTextField(); - textField.setColumns(columns); - } - /** * Convenience method for associating a label with the editor component. * @param label the label to associate diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/FindDialogTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/FindDialogTest.java index 47ef73560b..1a3245ea06 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/FindDialogTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/FindDialogTest.java @@ -4,9 +4,9 @@ * 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. @@ -63,6 +63,11 @@ public class FindDialogTest { // stub } + @Override + public void clearHighlights() { + // stub + } + @Override public SearchLocation search(String text, CursorPosition cursorPosition, boolean searchForward, boolean useRegex) { diff --git a/Ghidra/Framework/Gui/src/main/java/generic/test/AbstractGuiTest.java b/Ghidra/Framework/Gui/src/main/java/generic/test/AbstractGuiTest.java index b517d92c0e..8796e9f0df 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/test/AbstractGuiTest.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/test/AbstractGuiTest.java @@ -473,12 +473,12 @@ public class AbstractGuiTest extends AbstractGenericTest { public static AbstractButton findAbstractButtonByName(Container container, String name) { Component[] comp = container.getComponents(); for (Component element : comp) { - if ((element instanceof AbstractButton) && - name.equals(((AbstractButton) element).getName())) { - return (AbstractButton) element; + if ((element instanceof AbstractButton button) && + name.equals(button.getName())) { + return button; } - else if (element instanceof Container) { - AbstractButton b = findAbstractButtonByName((Container) element, name); + else if (element instanceof Container subContainer) { + AbstractButton b = findAbstractButtonByName(subContainer, name); if (b != null) { return b; }