mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-28 17:11:11 -09:00
GP-7114 - Search Text - Fixed searching of clipped text; fixed plate comment searching
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user