diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java index 0b75ecd2ee..69300967be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java @@ -15,17 +15,14 @@ */ package ghidra.app.plugin.core.navigation.locationreferences; -import java.awt.*; +import java.awt.Component; import java.util.*; -import javax.swing.*; -import javax.swing.text.View; - import org.apache.commons.lang3.StringUtils; import docking.widgets.search.SearchLocationContext; +import docking.widgets.search.SearchLocationContextRenderer; import docking.widgets.table.GTableCellRenderingData; -import generic.theme.GThemeDefaults.Colors; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.address.Address; @@ -33,7 +30,6 @@ import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.util.datastruct.Accumulator; import ghidra.util.exception.CancelledException; -import ghidra.util.layout.AbstractLayoutManager; import ghidra.util.table.AddressBasedTableModel; import ghidra.util.table.AddressPreviewTableModel; import ghidra.util.table.column.AbstractGhidraColumnRenderer; @@ -237,8 +233,14 @@ class LocationReferencesTableModel extends AddressBasedTableModel { - private JPanel htmlContainer = new JPanel(new HtmlTruncatingLayout()); - private JLabel ellipsisLabel = new JLabel("..."); + private SearchLocationContextRenderer contextRenderer = + new SearchLocationContextRenderer() { + @Override + protected SearchLocationContext getContext(GTableCellRenderingData d) { + LocationReference lr = (LocationReference) d.getRowObject(); + return lr.getContext(); + } + }; ContextCellRenderer() { setHTMLRenderingEnabled(true); @@ -247,9 +249,6 @@ class LocationReferencesTableModel extends AddressBasedTableModel { boolean isSelected = data.isSelected(); @@ -257,29 +256,13 @@ class LocationReferencesTableModel extends AddressBasedTableModel availableWidth && width != 0; - if (isClipped) { - availableWidth -= c2d.width; // save room for ellipsis - int c2x = availableWidth; - int c2y = insets.top; - c2.setBounds(c2x, c2y, c2d.width, c2d.height); - } - - c2.setVisible(isClipped); - - int c1x = insets.left; - int c1y = insets.top; - int cyh = d.height - (i.top + i.bottom); - c1.setBounds(c1x, c1y, availableWidth, cyh); - } - - } - } } 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 d9de313e31..3c26e083cd 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 @@ -244,14 +244,14 @@ public class DecompilerSearcher implements FindDialogSearcher { FieldLocation fieldLocation = new FieldLocation(i, lineInfo.fieldNumber(), lineInfo.row(), lineInfo.column()); int lineNumber = lineInfo.lineNumber(); - SearchLocationContext context = createContext(fullLine, match); + SearchLocationContext context = createContext(fullLine, lineNumber, match); return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1, searchString, true, field.getText(), lineNumber, context); } return null; } - private SearchLocationContext createContext(String line, SearchMatch match) { + private SearchLocationContext createContext(String line, int lineNumber, SearchMatch match) { SearchLocationContextBuilder builder = new SearchLocationContextBuilder(); int start = match.start; int end = match.end; @@ -261,6 +261,7 @@ public class DecompilerSearcher implements FindDialogSearcher { builder.append(line.substring(end)); } + builder.lineNumber(lineNumber); return builder.build(); } diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java index a212e056c2..8cb05189cf 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java @@ -246,15 +246,16 @@ public class DecompilerTextFinder { TextLine firstLine = lineMatches.get(0); int lineNumber = firstLine.getLineNumber(); AddressSet addresses = getAddresses(function, firstLine.getCLine()); - SearchLocationContext context = createMatchContext(lineMatches); + SearchLocationContext context = createMatchContext(lineMatches, lineNumber); TextMatch match = new TextMatch(function, addresses, lineNumber, searchText, context, true); callback.accept(match); } - private SearchLocationContext createMatchContext(List matches) { + private SearchLocationContext createMatchContext(List matches, int lineNumber) { SearchLocationContextBuilder builder = new SearchLocationContextBuilder(); + builder.lineNumber(lineNumber); for (TextLine line : matches) { if (!builder.isEmpty()) { builder.newline(); @@ -280,6 +281,7 @@ public class DecompilerTextFinder { } SearchLocationContextBuilder builder = new SearchLocationContextBuilder(); + builder.lineNumber(line.getLineNumber()); int start = matcher.start(); int end = matcher.end(); diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java index 1b9892fa2a..61e56ea833 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import java.util.regex.Pattern; import docking.widgets.search.SearchLocationContext; +import docking.widgets.search.SearchLocationContextRenderer; import docking.widgets.table.*; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; @@ -30,7 +31,6 @@ import ghidra.program.model.listing.*; import ghidra.util.datastruct.Accumulator; import ghidra.util.exception.CancelledException; import ghidra.util.table.GhidraProgramTableModel; -import ghidra.util.table.column.AbstractGhidraColumnRenderer; import ghidra.util.table.column.GColumnRenderer; import ghidra.util.table.field.AbstractProgramBasedDynamicTableColumn; import ghidra.util.table.field.FunctionNameTableColumn; @@ -164,37 +164,27 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel { + extends SearchLocationContextRenderer { - { - // the context uses html - setHTMLRenderingEnabled(true); + @Override + protected SearchLocationContext getContext(GTableCellRenderingData d) { + TextMatch m = (TextMatch) d.getRowObject(); + return m.getContext(); } @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { - // initialize - super.getTableCellRendererComponent(data); - TextMatch match = (TextMatch) data.getRowObject(); SearchLocationContext context = match.getContext(); - String text; if (match.isMultiLine()) { // multi-line matches create visual noise when showing colors, as of much of the // entire line matches - text = context.getPlainText(); + return renderPlainContext(data, context); } - else { - text = context.getBoldMatchingText(); - } - setText(text); - return this; + + return renderHtmlContext(data, context); } - @Override - public String getFilterString(SearchLocationContext context, Settings settings) { - return context.getPlainText(); - } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java index 2644dd8b12..20f4da6708 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java @@ -16,7 +16,6 @@ package docking.widgets; import java.awt.BorderLayout; -import java.awt.Component; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; @@ -30,14 +29,12 @@ import javax.swing.table.TableModel; import docking.ComponentProvider; import docking.Tool; import docking.action.DockingAction; -import docking.widgets.search.SearchLocationContext; -import docking.widgets.search.SearchResults; +import docking.widgets.search.*; import docking.widgets.table.*; import docking.widgets.table.actions.DeleteTableRowAction; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProviderStub; -import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; public class FindDialogResultsProvider extends ComponentProvider @@ -290,13 +287,17 @@ public class FindDialogResultsProvider extends ComponentProvider private class ContextColumn extends AbstractDynamicTableColumnStub { - private GColumnRenderer renderer = new ContextCellRenderer(); + private SearchLocationContextRenderer renderer = new SearchLocationContextRenderer() { + @Override + protected SearchLocationContext getContext(GTableCellRenderingData d) { + SearchLocation s = (SearchLocation) d.getRowObject(); + return s.getContext(); + } + }; @Override public SearchLocationContext getValue(SearchLocation rowObject, - Settings settings, - ServiceProvider sp) throws IllegalArgumentException { - + Settings settings, ServiceProvider sp) throws IllegalArgumentException { SearchLocationContext context = rowObject.getContext(); return context; } @@ -310,33 +311,6 @@ public class FindDialogResultsProvider extends ComponentProvider public GColumnRenderer getColumnRenderer() { return renderer; } - - private class ContextCellRenderer - extends AbstractGColumnRenderer { - - { - // the context uses html - setHTMLRenderingEnabled(true); - } - - @Override - public Component getTableCellRendererComponent(GTableCellRenderingData cellData) { - - // initialize - super.getTableCellRendererComponent(cellData); - - SearchLocation match = (SearchLocation) cellData.getRowObject(); - SearchLocationContext context = match.getContext(); - String text = context.getBoldMatchingText(); - setText(text); - return this; - } - - @Override - public String getFilterString(SearchLocationContext context, Settings settings) { - return context.getPlainText(); - } - } } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContext.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContext.java index e244fc1348..ff8073968f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContext.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContext.java @@ -26,7 +26,7 @@ import ghidra.util.HTMLUtilities; * * @see SearchLocationContextBuilder */ -public class SearchLocationContext { +public class SearchLocationContext implements Comparable { private static final String EMBOLDEN_START = ""; @@ -88,11 +88,27 @@ public class SearchLocationContext { } /** - * The full plain text of this context. + * The full plain text of this context. Any non-negative line number will be prepended to the + * text. * @return the text */ public String getPlainText() { - String lnText = getLineNumberText(false); + return getPlainText(true); + } + + /** + * Returns the plain text of this context, without html markup. + * + * @param includeLineNumber if true, any non-negative line number will be prepended to the text. + * @return the text + */ + public String getPlainText(boolean includeLineNumber) { + + String lnText = ""; + if (includeLineNumber) { + lnText = getLineNumberText(false); + } + StringBuilder buffy = new StringBuilder(lnText); for (Part part : parts) { buffy.append(part.getText()); @@ -124,8 +140,8 @@ public class SearchLocationContext { } /** - * Returns HTML text for this context. Any matching items embedded in the returned string will - * be bold. + * Returns HTML text for this context. Any matching items embedded in the returned string will + * be bold. Any non-negative line number will be prepended to the text. * @return the text */ public String getBoldMatchingText() { @@ -137,6 +153,27 @@ public class SearchLocationContext { return HTMLUtilities.HTML + buffy.toString(); } + /** + * Returns HTML text for this context. Any matching items embedded in the returned string will + * be bold. + * + * @param includeLineNumber if true, any non-negative line number will be prepended to the text. + * @return the text + */ + public String getBoldMatchingText(boolean includeLineNumber) { + + String lnText = ""; + if (includeLineNumber) { + lnText = getLineNumberText(false); + } + + StringBuilder buffy = new StringBuilder(lnText); + for (Part part : parts) { + buffy.append(part.getHtmlText()); + } + return HTMLUtilities.HTML + buffy.toString(); + } + /** * Returns any sub-strings of this context's overall text that match client-defined input * @@ -166,10 +203,55 @@ public class SearchLocationContext { return getPlainText(); } + @Override + public int compareTo(SearchLocationContext other) { + + // Use line numbers when both clients have them, as string integer comparisons do not + // naturally sort by integer value. + int l1 = getLineNumber(); + int l2 = other.getLineNumber(); + int result = Integer.compare(l1, l2); + if (result != 0) { + return result; + } + + // Note: the debug text will call out the portion of the line that matches. For + // multiple matches on the same line, we will have multiple rows. In that case, + // we need the match markup to help sort those lines. + String t1 = getDebugText(); + String t2 = other.getDebugText(); + return t1.compareTo(t2); + } + + @Override + public int hashCode() { + return Objects.hash(lineNumber, parts); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SearchLocationContext other = (SearchLocationContext) obj; + return lineNumber == other.lineNumber && Objects.equals(parts, other.parts); + } + +//================================================================================================= +// Inner Classes +//================================================================================================= + /** * A class that represents one or more characters within the full text of this context class */ static abstract class Part { + protected String text; Part(String text) { @@ -189,6 +271,26 @@ public class SearchLocationContext { return updated; } + @Override + public int hashCode() { + return Objects.hash(text); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Part other = (Part) obj; + return Objects.equals(text, other.text); + } + @Override public String toString() { return Json.toString(this); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContextRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContextRenderer.java new file mode 100644 index 0000000000..71d274f1a4 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContextRenderer.java @@ -0,0 +1,156 @@ +/* ### + * 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.search; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.text.View; + +import docking.widgets.table.GTableCellRenderingData; +import generic.theme.GThemeDefaults.Colors; +import ghidra.docking.settings.Settings; +import ghidra.util.layout.AbstractLayoutManager; +import ghidra.util.table.column.AbstractGColumnRenderer; + +/** + * A renderer for {@link SearchLocationContext}. This renderer handles the complexity of rendering + * html text with clipping. + */ +public abstract class SearchLocationContextRenderer + extends AbstractGColumnRenderer { + + private JPanel htmlContainer = new JPanel(new HtmlTruncatingLayout()); + private JLabel ellipsisLabel = new JLabel("..."); + + public SearchLocationContextRenderer() { + setHTMLRenderingEnabled(true); + } + + protected abstract SearchLocationContext getContext(GTableCellRenderingData data); + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + SearchLocationContext context = getContext(data); + return renderHtmlContext(data, context); + } + + public Component renderPlainContext(GTableCellRenderingData data, + SearchLocationContext context) { + + super.getTableCellRendererComponent(data); + + // Note: we do not include the line number prefix on the text, based on the assumption that + // clients of this renderer will have a separate line number column. + String text = context.getPlainText(false); + setText(text); + return this; + } + + public Component renderHtmlContext(GTableCellRenderingData data, + SearchLocationContext context) { + + // initialize + super.getTableCellRendererComponent(data); + + /* + We have html context. Build a renderer that is a panel with 2 children: the html label + (this renderer object) and an ellipsis label that will be visible as needed. + */ + + // Note: we do not include the line number prefix on the text, based on the assumption that + // clients of this renderer will have a separate line number column. + String html = context.getBoldMatchingText(false); + setText(html); + + ellipsisLabel.setOpaque(true); + ellipsisLabel.setForeground(Colors.FOREGROUND); + ellipsisLabel.setBackground(getBackground()); + + htmlContainer.setBackground(getBackground()); + htmlContainer.removeAll(); + htmlContainer.add(this); + htmlContainer.add(ellipsisLabel); + + return htmlContainer; + } + + @Override + public String getFilterString(SearchLocationContext rowObject, Settings settings) { + return rowObject.getPlainText(); + } + + /** + * A layout manager that positions 2 labels: a leading label with html and a trailing label + * with an ellipsis, which may not be visible. JLabels rendering html will not show an + * ellipsis when clipped. We use these 2 labels here to show when the leading html label's + * text is clipped. + */ + private class HtmlTruncatingLayout extends AbstractLayoutManager { + + @Override + public Dimension preferredLayoutSize(Container parent) { + + Dimension d = new Dimension(); + int n = parent.getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension cd = c.getPreferredSize(); + d.width += cd.width; + d.height = Math.max(d.height, cd.height); + } + + Insets insets = parent.getInsets(); + d.width += insets.left + insets.right; + d.height += insets.top + insets.bottom; + return d; + } + + @Override + public void layoutContainer(Container parent) { + // Assumption: the leading component is an html view; the trailing component is a + // label with an ellipsis + + JComponent c1 = (JComponent) parent.getComponent(0); + Dimension d = parent.getSize(); + Insets insets = parent.getInsets(); + int width = d.width - insets.left - insets.right; + + View v = (View) c1.getClientProperty("html"); + Insets i = c1.getInsets(); + int availableWidth = width - (i.left + i.right); + int htmlw = (int) v.getPreferredSpan(View.X_AXIS); + + JLabel c2 = (JLabel) parent.getComponent(1); + Dimension c2d = c2.getPreferredSize(); + boolean isClipped = htmlw > availableWidth && width != 0; + if (isClipped) { + availableWidth -= c2d.width; // save room for ellipsis + int c2x = availableWidth; + int c2y = insets.top; + c2.setBounds(c2x, c2y, c2d.width, c2d.height); + } + + c2.setVisible(isClipped); + + int c1x = insets.left; + int c1y = insets.top; + int cyh = d.height - (i.top + i.bottom); + c1.setBounds(c1x, c1y, availableWidth, cyh); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java index e3204021c8..6965ea1249 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java @@ -270,6 +270,7 @@ public class TextComponentSearcher implements FindDialogSearcher { private SearchLocationContext createContext(Line line, int start, int end) { SearchLocationContextBuilder builder = new SearchLocationContextBuilder(); + builder.lineNumber(line.lineNumber); String text = line.text(); int offset = line.offset(); // document offset int rstart = start - offset; // line-relative start