GP-7114 - Search Text - Fixed searching of clipped text; fixed plate comment searching

This commit is contained in:
dragonmacher
2026-08-12 19:01:15 -04:00
parent 54e3bd5f60
commit cccc0a6990
16 changed files with 627 additions and 155 deletions
@@ -19,6 +19,7 @@ import java.awt.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -29,9 +30,10 @@ import org.apache.commons.lang3.StringUtils;
import docking.*;
import docking.action.builder.ActionBuilder;
import docking.tool.ToolConstants;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.Highlight;
import docking.widgets.fieldpanel.field.TextField;
import docking.widgets.fieldpanel.support.*;
import docking.widgets.table.threaded.*;
import generic.json.Json;
import generic.theme.GIcon;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.*;
@@ -141,7 +143,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
searchDialog.setStatusText("");
SearchTask searchTask = (SearchTask) task;
Navigatable searchNavigatable = ((SearchTask) task).getNavigatable();
Navigatable searchNavigatable = searchTask.getNavigatable();
Program program = ((SearchTask) task).getProgram();
if (searchNavigatable.getProgram() == null || searchNavigatable.isDisposed()) {
return;
@@ -150,28 +152,38 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
TextSearchResult result = searchTask.getSearchLocation();
Searcher textSearcher = searchTask.getTextSearcher();
SearchOptions searchOptions = textSearcher.getSearchOptions();
if (result == null) {
searchDialog.setStatusText("Not found");
}
else {
searchDialog.setStatusText("");
ProgramLocation loc = result.programLocation();
if (goToService.goTo(searchNavigatable, loc, program)) {
new SearchTextHighlightProvider(searchNavigatable, searchOptions, null, program,
result);
// The navigatable may change its location if it does not have the search result
// location visible. We store that here so we can later detect that case in order
// to keep the search from getting stuck.
ProgramLocation navigatableLoc = navigatable.getLocation();
lastSearchHit = new LastSearchHit(loc, navigatableLoc);
}
}
lastSearchedText = searchOptions.getText();
if (task == currentTask) {
// The current task is used to prevent the program from closing and for cancelling. If
// they do not match, then a new search task has been started before this one finished.
currentTask = null;
}
if (result == null) {
searchDialog.setStatusText("Not found");
return;
}
ProgramLocation loc = result.programLocation();
if (lastSearchHit != null && lastSearchHit.shouldSkipHiddenMatch(loc)) {
searchAgainFromHiddenLocation(loc);
return;
}
searchDialog.setStatusText("");
if (!goToService.goTo(searchNavigatable, loc, program)) {
return;
}
// The navigatable may change its location if it does not have the search result
// location visible. We store that here so we can later detect that case in order
// to keep the search from getting stuck.
ProgramLocation navigatableLoc = navigatable.getLocation();
String searchText = searchOptions.getText();
lastSearchHit = new LastSearchHit(searchText, loc, navigatableLoc);
new SearchTextHighlightProvider(searchNavigatable, searchOptions, null, program, result);
}
String getLastSearchText() {
@@ -248,43 +260,49 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
searchDialog.setHasSelection(selection != null && !selection.isEmpty());
}
void next() {
ProgramLocation loc = getStartLocation();
Searcher textSearcher = null;
SearchOptions options = searchDialog.getSearchOptions();
AddressSetView addrs = getAddressSet(navigatable, options);
TaskMonitor monitor =
searchDialog.showTaskMonitorComponent(AbstractSearchTableModel.TITLE, true, true);
Program program = navigatable.getProgram();
if (options.isProgramDatabaseSearch()) {
textSearcher = new ProgramDatabaseSearcher(tool, program, loc, addrs, options, monitor);
}
else {
textSearcher = new ListingDisplaySearcher(tool, program, loc, addrs, options, monitor);
}
searchNext(navigatable.getProgram(), navigatable, textSearcher);
private void searchAgainFromHiddenLocation(ProgramLocation loc) {
lastSearchHit = lastSearchHit.createHiddenSearchHit(loc);
next();
}
private ProgramLocation getStartLocation() {
void next() {
ProgramLocation loc = getSearchStartLocation();
SearchOptions options = searchDialog.getSearchOptions();
AddressSetView addrs = getAddressSet(navigatable, options);
String title = AbstractSearchTableModel.TITLE;
TaskMonitor monitor = searchDialog.showTaskMonitorComponent(title, true, true);
Program p = navigatable.getProgram();
Searcher searcher = null;
if (options.isProgramDatabaseSearch()) {
searcher = new ProgramDatabaseSearcher(tool, p, loc, addrs, options, monitor);
}
else {
searcher = new ListingDisplaySearcher(tool, p, loc, addrs, options, monitor);
}
doSearchNext(navigatable.getProgram(), navigatable, searcher);
}
private ProgramLocation getSearchStartLocation() {
ProgramLocation currentNavLoc = navigatable.getLocation();
if (lastSearchHit != null) {
ProgramLocation lastNavLoc = lastSearchHit.navigatableLocation();
ProgramLocation lastNavLoc = lastSearchHit.getNavigatableLocation();
// The navigatable's location has not changed since the last search. Use the last search
// location as the start point for the next search. This ensures the searching does not
// get stuck when the navigatable cannot display the last search hit.
if (lastNavLoc.equals(currentNavLoc)) {
return lastSearchHit.searchLocation();
return lastSearchHit.getSearchLocation();
}
lastSearchHit = null;
}
return currentNavLoc;
}
private void searchNext(Program program, Navigatable searchNavigatable, Searcher textSearcher) {
private void doSearchNext(Program program, Navigatable searchNavigatable,
Searcher textSearcher) {
SearchTask task = new SearchTask(searchNavigatable, program, textSearcher);
task.addTaskListener(this);
currentTask = task;
@@ -553,9 +571,103 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
// Inner Classes
//==================================================================================================
private record LastSearchHit(ProgramLocation searchLocation,
ProgramLocation navigatableLocation) {
//
private class LastSearchHit {
private String searchText;
private ProgramLocation searchLocation;
private ProgramLocation navigatableLocation;
private boolean isHidden;
private int ellipsisStart;
LastSearchHit(String searchText, ProgramLocation searchLocation,
ProgramLocation navigatableLocation) {
this.searchText = searchText;
this.searchLocation = searchLocation;
this.navigatableLocation = navigatableLocation;
}
LastSearchHit createHiddenSearchHit(ProgramLocation loc) {
LastSearchHit newSearchHit = new LastSearchHit(searchText, loc, navigatableLocation);
newSearchHit.isHidden = true;
newSearchHit.ellipsisStart = ellipsisStart;
return newSearchHit;
}
/**
* Returns true if this search location is hidden and the given location is also in the
* clipped text area.
* @param loc the current search result location
* @return true to skip a hidden location
*/
boolean shouldSkipHiddenMatch(ProgramLocation loc) {
if (!isHidden) {
return false;
}
Class<? extends ProgramLocation> myClass = searchLocation.getClass();
Class<? extends ProgramLocation> otherClass = loc.getClass();
if (myClass != otherClass) {
return false;
}
Address myAddress = searchLocation.getAddress();
Address otherAddress = loc.getAddress();
if (!myAddress.equals(otherAddress)) {
return false;
}
// assume text is clipped per row
int myRow = searchLocation.getRow();
int otherRow = loc.getRow();
if (myRow != otherRow) {
return false;
}
// skip all text past the ellipsis, as it is not being rendered
int matchStart = loc.getCharOffset();
return matchStart > ellipsisStart;
}
ProgramLocation getSearchLocation() {
return searchLocation;
}
ProgramLocation getNavigatableLocation() {
return navigatableLocation;
}
/**
* Signals that this search result cannot be seen in the UI, such as when the content is
* in a field that has been clipped. This is called after the search when highlighting is
* taking place. We need to do this at that point because the highlighter is passed all the
* info it needs to see if the search result is hidden.
* @param hlSearchText the current highlight search text; used to know if the highlighter
* is working on the active search
* @param ellipsisOffset the offset of the ellipsis text
*/
void markHidden(String hlSearchText, int ellipsisOffset) {
if (!Objects.equals(searchText, hlSearchText)) {
return; // highlighting an older search hit
}
if (isHidden) {
return;
}
isHidden = true;
this.ellipsisStart = ellipsisOffset;
if (searchDialog != null) {
searchDialog.setStatusText("Search result hidden", MessageType.WARNING);
}
}
@Override
public String toString() {
return Json.toString(this);
}
}
private class TableLoadingListener implements ThreadedTableModelListener {
@@ -625,6 +737,9 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
private class SearchTextHighlightProvider
implements ListingHighlightProvider, ComponentProviderActivationListener {
private static final String ELLIPSIS = "...";
private SearchOptions searchOptions;
private TableComponentProvider<?> provider;
private Program highlightProgram;
@@ -655,10 +770,6 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory().getClass();
if (!doHighlight) {
return NO_HIGHLIGHTS;
}
if (checkRemoveHighlights()) {
return NO_HIGHLIGHTS;
}
@@ -687,6 +798,10 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
private Highlight[] getAllHighlights(String text, int cursorTextOffset) {
if (!doHighlight) {
return NO_HIGHLIGHTS;
}
String searchText = searchOptions.getText();
if (StringUtils.isBlank(searchText) || StringUtils.isBlank(text)) {
return NO_HIGHLIGHTS;
@@ -745,8 +860,9 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
*/
int row = fieldLocation.getRow();
int col = fieldLocation.getCol();
int screenOffset = field.screenLocationToTextOffset(row, col);
int searchStart = screenOffset;
int fullTextOffset = field.screenLocationToTextOffset(row, col);
int searchStart = fullTextOffset;
int searchEnd = searchStart + searchText.length();
Color hlColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR;
@@ -755,11 +871,106 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
hlColor = SearchConstants.SEARCH_HIGHLIGHT_CURRENT_ADDR_COLOR;
}
// Note: we still want to call these methods that handle clipped text, even when not
// highlighting (see the method notes).
// this handles fields that do their own clipping internally, like ClippingTextField
Highlight ellipsisHl =
createClippedDisplayHighlight(field, searchText, row, col, fullTextOffset);
if (ellipsisHl == null) {
// this handled fields that are clipped by the field factory, like PlateFieldFactory
ellipsisHl =
createClippedModelHighlight(field, text, searchText, fullTextOffset);
}
if (!doHighlight) {
return NO_HIGHLIGHTS;
}
if (ellipsisHl != null) {
return new Highlight[] { ellipsisHl };
}
// this is the matching search hit for a single search
int endInc = searchEnd - 1; // highlights are inclusive
return new Highlight[] { new Highlight(searchStart, endInc, hlColor) };
}
// Note: this method performs double-duty: it not only creates a highlight for text that
// is replaced with an ellipsis, it also updates the state of the plugin to track that the
// last search hit is a hidden search hit. Thus, even if highlighting is off, we still need
// to perform this work for the searching to perform optimally when ignoring hidden results.
private Highlight createClippedDisplayHighlight(ListingField field,
String searchText, int row, int col, int fullTextOffset) {
if (!(field instanceof TextField tf)) {
return null;
}
boolean isClipped = tf.isClipped();
if (!isClipped) {
return null;
}
int numCols = tf.getNumCols(row);
int fieldEnd = numCols - 1; // -1 for zero based indexing
int clippedCol = col;
if (clippedCol != fieldEnd) {
// Assume the hit landing at the end is required for a hidden result. Search results
// are normally placed at the beginning of the text.
return null;
}
// signal to have the next search skip any more hidden text hits
lastSearchHit.markHidden(searchText, clippedCol);
// A normal highlight will work for fields that can paint their own clipping. These
// fields will paint the '...' and a highlight on the ellipsis.
int start = fullTextOffset;
int end = fullTextOffset + ELLIPSIS.length();
Color hlColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR;
return new Highlight(start, end, hlColor);
}
// Note: this method performs double-duty: it not only creates a highlight for text that
// is replaced with an ellipsis, it also updates the state of the plugin to track that the
// last search hit is a hidden search hit. Thus, even if highlighting is off, we still need
// to perform this work for the searching to perform optimally when ignoring hidden results.
private Highlight createClippedModelHighlight(ListingField field, String fieldText,
String searchText, int screenOffset) {
int len = ELLIPSIS.length();
int contextStart = screenOffset - len;
int contextEnd = contextStart + (len * 2);
if (contextStart < 0) {
return null;
}
String context = fieldText.substring(contextStart, contextEnd);
int relativeIndex = context.indexOf(ELLIPSIS);
if (relativeIndex < 0) {
return null;
}
if (searchText.contains(ELLIPSIS)) {
return null;
}
Color hlColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR;
int absoluteIndex = contextStart + relativeIndex;
Highlight ellipsisHl = new Highlight(absoluteIndex, absoluteIndex + len, hlColor);
// this fields search result is hidden behind truncated text
int screenEllipsisOffset = ellipsisHl.getStart();
RowColLocation modelLoc = field.textOffsetToScreenLocation(screenEllipsisOffset);
int modelEllipsisOffset = modelLoc.col();
lastSearchHit.markHidden(searchText, modelEllipsisOffset);
return ellipsisHl;
}
private boolean shouldHighlight(ListingField field) {
ProxyObj<?> proxy = field.getProxy();
@@ -534,12 +534,22 @@ public class EolCommentFieldFactory extends FieldFactory {
codeUnitFormatOptions.followReferencedPointers(), maxDisplayLines, extraCommentsOption);
ListingTextField btf = (ListingTextField) bf;
RowColLocation eolRowCol = displayableEol.getRowCol((CommentFieldLocation) loc);
RowColLocation rcl = btf.dataToScreenLocation(eolRowCol.row(), eolRowCol.col());
if (!hasSamePath(bf, loc)) {
return null;
}
return new FieldLocation(index, fieldNum, rcl.row(), rcl.col());
RowColLocation eolRowCol = displayableEol.getRowCol((CommentFieldLocation) loc);
int row = eolRowCol.row();
int col = eolRowCol.col();
RowColLocation screenRowCol = btf.dataToScreenLocation(row, col);
int screenRow = screenRowCol.row();
int screenCol = screenRowCol.col();
if (screenRowCol.isHidden()) { // clipped location
screenCol = btf.getNumCols(screenRow) - 1; // -1 for zero based indexing
}
return new FieldLocation(index, fieldNum, screenRow, screenCol);
}
@Override
@@ -571,8 +581,8 @@ public class EolCommentFieldFactory extends FieldFactory {
if (comments.length == 0) {
return null;
}
StringBuffer buf = new StringBuffer(comments[0]);
StringBuilder buf = new StringBuilder(comments[0]);
for (int i = 1; i < comments.length; i++) {
buf.append(separatorChar + comments[i]);
}
@@ -227,8 +227,8 @@ abstract class OperandFieldHelper extends FieldFactory {
return null;
}
ListingTextField btf = (ListingTextField) lf;
FieldElement fieldElement = btf.getFieldElement(row, col);
ListingTextField ltf = (ListingTextField) lf;
FieldElement fieldElement = ltf.getFieldElement(row, col);
if (!(fieldElement instanceof OperandFieldElement)) {
return null;
}
@@ -236,7 +236,7 @@ abstract class OperandFieldHelper extends FieldFactory {
OperandFieldElement element = (OperandFieldElement) fieldElement;
int opIndex = element.getOperandIndex();
int subOpIndex = element.getOperandSubIndex();
RowColLocation translatedLocation = btf.screenToDataLocation(row, col);
RowColLocation translatedLocation = ltf.screenToDataLocation(row, col);
int dataCol = translatedLocation.col();
if (obj instanceof Instruction) {
@@ -935,8 +935,8 @@ abstract class OperandFieldHelper extends FieldFactory {
@Override
public FieldElement replaceAll(char[] targets, char replacement) {
return new OperandFieldElement(attributedString.replaceAll(targets, replacement), row,
operandSubIndex, column);
AttributedString as = attributedString.replaceAll(targets, replacement);
return new OperandFieldElement(as, row, operandSubIndex, column);
}
}
@@ -241,6 +241,8 @@ public class PlateFieldFactory extends FieldFactory {
// add top border
elements.add(new TextFieldElement(asteriscs, row++, 0));
int rowOffset = elements.size(); // any header lines and our top border
// add and word wrap the comments
List<FieldElement> commentsList = new ArrayList<>();
for (String c : comments) {
@@ -260,7 +262,7 @@ public class PlateFieldFactory extends FieldFactory {
commentsList = FieldUtils.wrap(commentsList, Math.max(width - paddingWidth, charWidth));
}
boolean isClipped = addSideBorders(commentsList);
boolean isClipped = addSideBorders(commentsList, rowOffset);
elements.addAll(commentsList);
// add bottom border
@@ -269,11 +271,13 @@ public class PlateFieldFactory extends FieldFactory {
return isClipped;
}
private boolean addSideBorders(List<FieldElement> comments) {
private boolean addSideBorders(List<FieldElement> comments, int rowOffset) {
boolean isClipped = false;
for (int i = 0; i < comments.size(); i++) {
FieldElementResult result = addSideBorder(comments.get(i), i, false);
FieldElement element = comments.get(i);
int row = rowOffset + i;
FieldElementResult result = addSideBorder(element, row, false);
isClipped |= result.isClipped();
comments.set(i, result.getFieldElement());
}
@@ -293,13 +297,16 @@ public class PlateFieldFactory extends FieldFactory {
int sideSpaceWidth = 2 * spaceWidth;
int availableWidth = fullStarWidth - sideStarWidth - sideSpaceWidth;
if (availableWidth < element.getStringWidth()) {
// not enough room; clip the text and add ellipses
// not enough room; clip the text and add ellipsis
isClipped = true;
ellipsisText = ELLIPSIS;
ellipsisWidth = getMetrics().charWidth('.') * ELLIPSIS.length();
availableWidth -= ellipsisWidth;
int charsThatFit = element.getMaxCharactersForWidth(availableWidth);
FieldElement fullElement = element;
element = element.substring(0, charsThatFit); // clip
element = new ClippedPlateRowFieldElement(fullElement, element);
}
int paddingWidth = sideStarWidth + sideSpaceWidth;
@@ -313,24 +320,28 @@ public class PlateFieldFactory extends FieldFactory {
buffy.append('*').append(' ');
addPaddingSpaces(buffy, prePaddingCharCount);
FieldElement prefix = new TextFieldElement(
new AttributedString(buffy.toString(), CommentColors.PLATE, getMetrics()), row, 0);
AttributedString as = toAttrString(buffy.toString());
FieldElement prefix = new TextFieldElement(as, row, 0);
FieldElement ellipsis = new TextFieldElement(
new AttributedString(ellipsisText, CommentColors.PLATE, getMetrics()), row,
prefix.length() + element.length());
int col = prefix.length() + element.length();
as = toAttrString(ellipsisText);
FieldElement ellipsis = new TextFieldElement(as, row, col);
buffy.setLength(0);
addPaddingSpaces(buffy, postPaddingCharCount);
buffy.append(' ').append('*');
FieldElement suffix = new TextFieldElement(
new AttributedString(buffy.toString(), CommentColors.PLATE, getMetrics()), row,
prefix.length() + element.length() + ellipsis.length());
col = col + ellipsis.length();
as = toAttrString(buffy.toString());
FieldElement suffix = new TextFieldElement(as, row, col);
return new FieldElementResult(
new CompositeFieldElement(new FieldElement[] { prefix, element, ellipsis, suffix }),
isClipped);
CompositeFieldElement fullElement =
new PlateCommentRowElement(new FieldElement[] { prefix, element, ellipsis, suffix });
return new FieldElementResult(fullElement, isClipped);
}
private AttributedString toAttrString(String s) {
return new AttributedString(s, CommentColors.PLATE, getMetrics());
}
private void addPaddingSpaces(StringBuilder buf, int count) {
@@ -509,8 +520,10 @@ public class PlateFieldFactory extends FieldFactory {
commentRow = -1; // clicked above the comment or the bottom decoration line
}
return new PlateFieldLocation(cu.getProgram(), ((CodeUnit) proxyObject).getMinAddress(),
cpath, commentRow, dataLocation.col(), comments, commentRow);
Program p = cu.getProgram();
Address addr = cu.getMinAddress();
int charOffset = dataLocation.col();
return new PlateFieldLocation(p, addr, cpath, commentRow, charOffset, comments, commentRow);
}
private int getNumberOfLeadingFillerLines(ListingField listingField) {
@@ -526,11 +539,10 @@ public class PlateFieldFactory extends FieldFactory {
public FieldLocation getFieldLocation(ListingField listingField, BigInteger index, int fieldNum,
ProgramLocation programLoc) {
if (!(programLoc instanceof CommentFieldLocation)) {
if (!(programLoc instanceof CommentFieldLocation commentLocation)) {
return null;
}
CommentFieldLocation commentLocation = (CommentFieldLocation) programLoc;
if (commentLocation.getCommentType() != CommentType.PLATE) {
return null;
}
@@ -570,15 +582,45 @@ public class PlateFieldFactory extends FieldFactory {
}
}
/*
This field adds decoration lines to the model lines. The 'data row' is the raw model
data that is the comments plus the header blank lines and asteriscs.
*/
int commentRow = commentLocation.getRow();
int numberBlankLines = getNumberBlankLines(cu, hasComment);
int headerCount = hasComment ? 1 : 0;
int dataRow = commentRow + numberBlankLines + headerCount;
ListingTextField listingTf = (ListingTextField) listingField;
int charOffset = commentLocation.getCharOffset();
FieldLocation clippedLocation =
getClippedFieldLocation(listingTf, index, fieldNum, dataRow, charOffset);
if (clippedLocation != null) {
return clippedLocation;
}
ListingTextField listingTextField = (ListingTextField) listingField;
RowColLocation location =
listingTextField.dataToScreenLocation(dataRow, commentLocation.getCharOffset());
RowColLocation location = listingTf.dataToScreenLocation(dataRow, charOffset);
return new FieldLocation(index, fieldNum, location.row(), location.col());
}
private FieldLocation getClippedFieldLocation(ListingTextField listingTextField,
BigInteger index, int fieldNum, int dataRow, int charOffset) {
PlateListingTextField pltf = (PlateListingTextField) listingTextField;
PlateFieldTextField ptf = pltf.getPlateTextField();
PlateCommentRowElement element = ptf.getPlateCommentElement(dataRow);
if (element == null || !element.isClipped()) {
return null;
}
String clippedText = element.getClippedText();
if (charOffset > clippedText.length()) {
// place text at the end of the ellipsis
int col = element.getEllipsisEnd();
return new FieldLocation(index, fieldNum, dataRow, col);
}
return null;
}
@Override
@@ -754,15 +796,27 @@ public class PlateFieldFactory extends FieldFactory {
private boolean isCommentClipped;
private String commentText;
private List<FieldElement> textElements;
PlateFieldTextField(List<FieldElement> textElements, PlateFieldFactory factory,
ProxyObj<?> proxy, int startX, int width, String commentText,
boolean isCommentClipped, FieldHighlightFactory hlFactory) {
super(textElements, startX, width, Integer.MAX_VALUE, hlFactory);
this.textElements = textElements;
this.commentText = commentText;
this.isCommentClipped = isCommentClipped;
}
public PlateCommentRowElement getPlateCommentElement(int viewRow) {
FieldElement element = textElements.get(viewRow);
if (!(element instanceof PlateCommentRowElement)) {
return null; // this row is not a comment row
}
return (PlateCommentRowElement) element;
}
@Override
public boolean isClipped() {
return isCommentClipped;
@@ -798,6 +852,66 @@ public class PlateFieldFactory extends FieldFactory {
}
}
/**
* A composite element for a row of text inside of the plate comment.
*/
private static class PlateCommentRowElement extends CompositeFieldElement {
public PlateCommentRowElement(FieldElement[] elements) {
super(elements);
}
int getEllipsisEnd() {
int prefixCol = 0;
int commentCol = 1;
FieldElement prefix = getFieldElementByIndex(prefixCol);
FieldElement comment = getFieldElementByIndex(commentCol);
return prefix.length() + comment.length() + ELLIPSIS.length();
}
String getClippedText() {
int commentCol = 1;
FieldElement comment = getFieldElementByIndex(commentCol);
return comment.getText();
}
boolean isClipped() {
// Our structure when constructed of field elements:
// prefix, comment, ellipsis, suffix
// (where ellipsis will be empty when not clipped)
int commentCol = 1;
FieldElement comment = getFieldElementByIndex(commentCol);
return comment instanceof ClippedPlateRowFieldElement;
}
}
/**
* A class that represents clipped comment content. It has the clipped content and the full
* content.
*/
private static class ClippedPlateRowFieldElement extends WrappedFieldElement {
private FieldElement fullElement;
protected ClippedPlateRowFieldElement(FieldElement fullElement, FieldElement delegate) {
super(delegate);
this.fullElement = fullElement;
}
@Override
public FieldElement substring(int start, int end) {
FieldElement newDelegate = super.substring(start, end);
return new ClippedPlateRowFieldElement(fullElement, newDelegate);
}
@Override
public FieldElement replaceAll(char[] targets, char replacement) {
FieldElement newDelegate = super.replaceAll(targets, replacement);
return new ClippedPlateRowFieldElement(fullElement, newDelegate);
}
}
private class FieldElementResult {
private FieldElement element;
private boolean isClipped;
@@ -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.
@@ -120,8 +120,10 @@ abstract public class AbstractTextFieldElement implements FieldElement {
@Override
public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) {
if (dataRow == row && (dataColumn >= column) && (dataColumn <= column + length())) {
return dataColumn - column;
int start = column;
int end = start + length();
if (dataRow == row && dataColumn >= start && dataColumn <= end) {
return dataColumn - start;
}
return -1;
@@ -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.
@@ -249,7 +249,6 @@ public class ClippingTextField implements TextField {
}
void print(Graphics g, PaintContext context) {
// TODO fix printing
textElement.paint(null, g, startX, 0);
if (isClipped) {
paintDots(g, startX + textElement.getStringWidth());
@@ -337,15 +336,6 @@ public class ClippingTextField implements TextField {
}
}
/**
* Converts a single column value into a MultiStringLocation which specifies
* a string index and a column position within that string.
*
* @param screenColumn
* the overall column position in the total String.
* @return MultiStringLocation the MultiStringLocation corresponding to the
* given column.
*/
@Override
public RowColLocation screenToDataLocation(int screenRow, int screenColumn) {
return originalElement.getDataLocationForCharacterIndex(screenColumn);
@@ -354,15 +344,19 @@ public class ClippingTextField implements TextField {
@Override
public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) {
int column = textElement.getCharacterIndexForDataLocation(dataRow, dataColumn);
int column = originalElement.getCharacterIndexForDataLocation(dataRow, dataColumn);
if (column < 0) {
column = textElement.getCharacterIndexForDataLocation(dataRow, dataColumn - 3);
if (column < 0) {
return new DefaultRowColLocation(0, textElement.length());
}
return new RowColLocation(0, textElement.length());
return new DefaultRowColLocation(0, textElement.length());
}
return new RowColLocation(0, column);
// the text of this field does not contain the '...' when clipped, that is added when
// rendered by the paint() method
int screenLength = textElement.length();
boolean locationClipped = column > screenLength;
// Note: the column value we return is 0, as the client will update the row when using
// multiple clipping text field rows.
return new RowColLocation(0, column, locationClipped);
}
private int findX(int col) {
@@ -372,9 +366,6 @@ public class ClippingTextField implements TextField {
return textElement.substring(0, col).getStringWidth();
}
/**
* Returns true if the text is clipped (truncated)
*/
@Override
public boolean isClipped() {
return isClipped;
@@ -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.
@@ -208,6 +208,10 @@ public class CompositeFieldElement implements FieldElement {
return fieldElements.length;
}
protected FieldElement getFieldElementByIndex(int index) {
return fieldElements[index];
}
//==================================================================================================
// Location Info
//==================================================================================================
@@ -220,18 +224,14 @@ public class CompositeFieldElement implements FieldElement {
@Override
public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) {
int columnsSoFar = 0;
for (int i = fieldElements.length - 1; i >= 0; i--) {
columnsSoFar += fieldElements[i].length();
for (int i = 0; i < fieldElements.length; i++) {
int column = fieldElements[i].getCharacterIndexForDataLocation(dataRow, dataColumn);
if (column != -1) {
// column value is relative to the current field; convert it to this field's offset
int fieldStart = length() - columnsSoFar;
return fieldStart + column;
return columnsSoFar + column;
}
columnsSoFar += fieldElements[i].length();
}
return -1;
}
@@ -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.
@@ -582,7 +582,6 @@ public class CompositeVerticalLayoutTextField implements TextField {
private class FieldRow {
private TextField field;
private int displayRowOffset;
private int yOffset;
FieldRow(TextField field, int rowOffset, int yOffset) {
this.field = field;
@@ -598,10 +597,6 @@ public class CompositeVerticalLayoutTextField implements TextField {
return relativeRow + displayRowOffset;
}
int getY() {
return yOffset;
}
@Override
public String toString() {
return Json.toString(this);
@@ -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.
@@ -192,7 +192,7 @@ public interface Field {
public String getTextWithLineSeparators();
/**
* Returns the row, column position for an offset into the string returned by getText()
* Returns the row, column position for an offset into the string returned by getText()
* @param textOffset the offset into the entire text string for this field
* @return a RowColLocation that contains the row,column location in the field for a position in
* the overall field text
@@ -206,4 +206,12 @@ public interface Field {
* @return the offset
*/
public int screenLocationToTextOffset(int row, int col);
/**
* Returns true if the field is not displaying all the text information
* @return true if the field is not displaying all the text information
*/
public default boolean isClipped() {
return false;
}
}
@@ -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.
@@ -43,12 +43,6 @@ public interface TextField extends Field {
*/
public RowColLocation dataToScreenLocation(int dataRow, int dataColumn);
/**
* Returns true if the field is not displaying all the text information
* @return true if the field is not displaying all the text information
*/
public boolean isClipped();
/**
* Returns the FieldElement at the given screen location.
* @param screenRow the row on the screen
@@ -482,7 +482,7 @@ public class VerticalLayoutTextField implements TextField {
// A DefaultRowColLocation means that the line did not have an exact match for
// the dataRow and dataColumn, so need to keep looking at each line.
if (!(loc instanceof DefaultRowColLocation)) {
return new RowColLocation(i, loc.col());
return loc.withRow(i);
}
}
@@ -0,0 +1,116 @@
/* ###
* 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.fieldpanel.field;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JComponent;
import docking.widgets.fieldpanel.support.RowColLocation;
/**
* A simple field element that wraps another field element. This class allows clients to wrap field
* elements without having to re-implement all of the methods themselves.
*/
public abstract class WrappedFieldElement implements FieldElement {
private FieldElement delegate;
public WrappedFieldElement(FieldElement delegate) {
this.delegate = delegate;
}
@Override
public String getText() {
return delegate.getText();
}
@Override
public int length() {
return delegate.length();
}
@Override
public int getStringWidth() {
return delegate.getStringWidth();
}
@Override
public int getHeightAbove() {
return delegate.getHeightAbove();
}
@Override
public int getHeightBelow() {
return delegate.getHeightBelow();
}
@Override
public char charAt(int index) {
return delegate.charAt(index);
}
@Override
public Color getColor(int charIndex) {
return delegate.getColor(charIndex);
}
@Override
public FieldElement substring(int start) {
return delegate.substring(start);
}
@Override
public FieldElement substring(int start, int end) {
return delegate.substring(start, end);
}
@Override
public FieldElement replaceAll(char[] targets, char replacement) {
return delegate.replaceAll(targets, replacement);
}
@Override
public int getMaxCharactersForWidth(int width) {
return delegate.getMaxCharactersForWidth(width);
}
@Override
public RowColLocation getDataLocationForCharacterIndex(int characterIndex) {
return delegate.getDataLocationForCharacterIndex(characterIndex);
}
@Override
public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) {
return delegate.getCharacterIndexForDataLocation(dataRow, dataColumn);
}
@Override
public void paint(JComponent c, Graphics g, int x, int y) {
delegate.paint(c, g, x, y);
}
@Override
public FieldElement getFieldElement(int column) {
return delegate.getFieldElement(column);
}
@Override
public String toString() {
return delegate.toString();
}
}
@@ -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.
@@ -15,12 +15,15 @@
*/
package docking.widgets.fieldpanel.support;
import java.util.Objects;
/**
* Simple class to return a row, column location.
*/
public class RowColLocation {
protected int row;
protected int col;
private boolean isHidden;
/**
* Constructs a new RowColLocation with the given row and column.
@@ -30,6 +33,19 @@ public class RowColLocation {
public RowColLocation(int row, int col) {
this.row = row;
this.col = col;
this.isHidden = false;
}
/**
* Constructs a new RowColLocation with the given row and column.
* @param row the row location
* @param col the column location
* @param isHidden true if the given location is replaced with ellipsis in the UI
*/
public RowColLocation(int row, int col, boolean isHidden) {
this.row = row;
this.col = col;
this.isHidden = isHidden;
}
public int row() {
@@ -40,26 +56,30 @@ public class RowColLocation {
return col;
}
/**
* {@return true if this location is hidden, such as when the location is replaced by an ellipsis
* in the UI}
*/
public boolean isHidden() {
return isHidden;
}
public RowColLocation withCol(int newColumn) {
return new RowColLocation(row, newColumn);
return new RowColLocation(row, newColumn, isHidden);
}
public RowColLocation withRow(int newRow) {
return new RowColLocation(newRow, col);
return new RowColLocation(newRow, col, isHidden);
}
@Override
public String toString() {
return row + "," + col;
return row + "," + col + (isHidden ? ", hidden" : "");
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + col;
result = prime * result + row;
return result;
return Objects.hash(row, col, isHidden);
}
@Override
@@ -73,6 +93,6 @@ public class RowColLocation {
}
RowColLocation loc = (RowColLocation) object;
return (row == loc.row) && (col == loc.col);
return row == loc.row && col == loc.col && isHidden == loc.isHidden;
}
}
@@ -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.
@@ -114,7 +114,7 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest {
assertEquals(new RowColLocation(1, 4), textField.dataToScreenLocation(2, 4));
// Supercalifra (12 chars); ... (3 chars); Supercalifra... (15 chars)
assertEquals(new RowColLocation(1, 12), textField.dataToScreenLocation(2, 15));
assertEquals(new RowColLocation(1, 15, true), textField.dataToScreenLocation(2, 15));
assertEquals(new RowColLocation(2, 0), textField.dataToScreenLocation(3, 0));
assertEquals(new RowColLocation(2, 4), textField.dataToScreenLocation(3, 4));
@@ -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.
@@ -92,7 +92,9 @@ public class VerticalLayoutTextFieldTest extends AbstractGenericTest {
assertEquals(new RowColLocation(2, 0), field.dataToScreenLocation(2, 0));
assertEquals(new RowColLocation(2, 4), field.dataToScreenLocation(2, 4));
assertEquals(new RowColLocation(2, 12), field.dataToScreenLocation(2, 12));
assertEquals(new RowColLocation(2, 12), field.dataToScreenLocation(2, 15));
// clipped text reports as hidden
assertEquals(new RowColLocation(2, 15, true), field.dataToScreenLocation(2, 15));
assertEquals(new RowColLocation(3, 0), field.dataToScreenLocation(3, 0));
assertEquals(new RowColLocation(3, 4), field.dataToScreenLocation(3, 4));
@@ -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.
@@ -162,7 +162,8 @@ public class CompositeVerticalLayoutTextFieldTest extends AbstractGenericTest {
assertRowCol(2, 0, field.dataToScreenLocation(2, 0));
assertRowCol(2, 4, field.dataToScreenLocation(2, 4));
assertRowCol(2, 12, field.dataToScreenLocation(2, 12));
assertRowCol(2, 12, field.dataToScreenLocation(2, 15));
assertRowColClipped(2, 15, field.dataToScreenLocation(2, 15));
assertRowCol(3, 0, field.dataToScreenLocation(3, 0));
assertRowCol(3, 4, field.dataToScreenLocation(3, 4));
@@ -584,4 +585,12 @@ public class CompositeVerticalLayoutTextFieldTest extends AbstractGenericTest {
assertEquals("Wrong row", expectedRow, actualLocation.row());
assertEquals("Wrong column", expectedColumn, actualLocation.col());
}
private void assertRowColClipped(int expectedRow, int expectedColumn,
RowColLocation actualLocation) {
assertEquals("Wrong row", expectedRow, actualLocation.row());
assertEquals("Wrong column", expectedColumn, actualLocation.col());
assertTrue("Text should have been clipped", actualLocation.isHidden());
}
}