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

View File

@@ -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();

View File

@@ -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]);
}

View File

@@ -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);
}
}

View File

@@ -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;