From cccc0a6990f9dcaedecaa7ee873b459fd6e4a20e Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:01:15 -0400 Subject: [PATCH] GP-7114 - Search Text - Fixed searching of clipped text; fixed plate comment searching --- .../core/searchtext/SearchTextPlugin.java | 311 +++++++++++++++--- .../viewer/field/EolCommentFieldFactory.java | 18 +- .../util/viewer/field/OperandFieldHelper.java | 10 +- .../util/viewer/field/PlateFieldFactory.java | 158 +++++++-- .../field/AbstractTextFieldElement.java | 10 +- .../fieldpanel/field/ClippingTextField.java | 35 +- .../field/CompositeFieldElement.java | 18 +- .../CompositeVerticalLayoutTextField.java | 9 +- .../widgets/fieldpanel/field/Field.java | 14 +- .../widgets/fieldpanel/field/TextField.java | 10 +- .../field/VerticalLayoutTextField.java | 2 +- .../fieldpanel/field/WrappedFieldElement.java | 116 +++++++ .../fieldpanel/support/RowColLocation.java | 42 ++- .../fieldpanel/FlowLayoutTextFieldTest.java | 6 +- .../VerticalLayoutTextFieldTest.java | 8 +- .../CompositeVerticalLayoutTextFieldTest.java | 15 +- 16 files changed, 627 insertions(+), 155 deletions(-) create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappedFieldElement.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/SearchTextPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/SearchTextPlugin.java index 04ec38315c..ef83e4a539 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/SearchTextPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/SearchTextPlugin.java @@ -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 myClass = searchLocation.getClass(); + Class 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 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(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java index 2a5c2b6a02..7100a7861c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java @@ -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]); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OperandFieldHelper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OperandFieldHelper.java index ff6cdf0ff1..64b36082a3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OperandFieldHelper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OperandFieldHelper.java @@ -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); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java index a33ddb1582..11ad47d7a8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java @@ -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 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 comments) { + private boolean addSideBorders(List 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 textElements; PlateFieldTextField(List 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; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AbstractTextFieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AbstractTextFieldElement.java index 2969e64b4a..68e1fe8b28 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AbstractTextFieldElement.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AbstractTextFieldElement.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ClippingTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ClippingTextField.java index fc41b6aacb..06dbc1107a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ClippingTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ClippingTextField.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeFieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeFieldElement.java index 80a557582e..1f74d90ff7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeFieldElement.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeFieldElement.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextField.java index 29a6fe3794..d7b4f704ea 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextField.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/Field.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/Field.java index dd378d7ca5..4a68a4bde4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/Field.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/Field.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextField.java index 9a8789d7f1..afb85dd100 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextField.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/VerticalLayoutTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/VerticalLayoutTextField.java index 1183acdf18..dd92facd12 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/VerticalLayoutTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/VerticalLayoutTextField.java @@ -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); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappedFieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappedFieldElement.java new file mode 100644 index 0000000000..7afab5f0a6 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappedFieldElement.java @@ -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(); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowColLocation.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowColLocation.java index 6821b56458..1f922bee7e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowColLocation.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowColLocation.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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; } } diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/FlowLayoutTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/FlowLayoutTextFieldTest.java index acc82dca7e..7362386f8e 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/FlowLayoutTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/FlowLayoutTextFieldTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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)); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/VerticalLayoutTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/VerticalLayoutTextFieldTest.java index 55c40c7727..b9b0160f5d 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/VerticalLayoutTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/VerticalLayoutTextFieldTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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)); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextFieldTest.java index 511d681a48..6ba0cd5dd6 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextFieldTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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()); + } + }