diff --git a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm index 2b7eccdf47..5f988a63af 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm @@ -927,22 +927,43 @@

Display Local Block - Prepends the name of the memory block containing the XREF source address to each XREF.

+

Namespace Options: + +

+

Display Non-local Namespace - Select this option to prepend the namespace to all + XREFs that are not from an instruction within the current Function's body.  Currently, + this would only affect XREFs that originate in some other function.
+

+ +

Display Library in Namespace - Include the library name in the namespace.
+

+ +

Display Local Namespace - Select this option to prepend the namespace to all + XREFs that are from the current Function.
+

+
+

Use Local Namespace Override - Select this + option to show a fixed prefix for local XREFs instead of the function's name.  This + option is only available if the "Display Local Namespace" option is on.  The text box + contains the prefix to use for local XREFs.

+
+
+ +

Display Reference Type - Shows a single letter to represent the type of reference. + Some of the possible types are: + Read (R), Write (W), Data (*), Call (c), Jump (j) and Thunk (T). + +

Group by Function - Groups all references by the containing source function. + With this option off, all references within a function are displayed on their on row. + With this feature on, each function will get a single row, with all references displayed on + that row. +

Maximum Number of XREFs To Display - The maximum number of lines used to display XREFs. Additional XREFs will not be displayed.

- -

Display Non-local Namespace - Select this option to prepend the namespace to all - XREFs that are not from an instruction within the current Function's body.  Currently, - this would only affect XREFs that originate in some other function.
-

- -

Display Local Namespace - Select this option to prepend the namespace to all - XREFs that are from the current Function.
-

- -

Use Local Namespace Override - Select this - option to show a fixed prefix for local XREFs instead of the function's name.  This - option is only available if the "Display Local Namespace" option is on.  The text box - contains the prefix to use for local XREFs.

+ +

Sort References by - Allows the references to be sorted by Address or by type. + This is most useful when Group by Function is off. + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserPlugin.java index e58c3d4a4c..6c800cf20f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserPlugin.java @@ -988,8 +988,8 @@ public class CodeBrowserPlugin extends Plugin return; // not sure if this can happen } - Set refs = XReferenceUtil.getAllXrefs(location); - XReferenceUtil.showAllXrefs(connectedProvider, tool, service, location, refs); + Set refs = XReferenceUtils.getAllXrefs(location); + XReferenceUtils.showXrefs(connectedProvider, tool, service, location, refs); } private GhidraProgramTableModel

createTableModel(CodeUnitIterator iterator, @@ -1065,16 +1065,13 @@ public class CodeBrowserPlugin extends Plugin */ public boolean goToField(Address a, String fieldName, int occurrence, int row, int col, boolean scroll) { - - boolean result = SystemUtilities - .runSwingNow(() -> doGoToField(a, fieldName, occurrence, row, col, scroll)); - return result; + return Swing.runNow(() -> doGoToField(a, fieldName, occurrence, row, col, scroll)); } private boolean doGoToField(Address a, String fieldName, int occurrence, int row, int col, boolean scroll) { - SystemUtilities.assertThisIsTheSwingThread("GoTo must be performed on the Swing thread"); + Swing.assertSwingThread("'Go To' must be performed on the Swing thread"); // make sure that the code browser is ready to go--sometimes it is not, due to timing // during the testing process, like when the tool is first loaded. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtil.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtil.java index b02d7b2d6b..bc1d5294c4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtil.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtil.java @@ -32,10 +32,12 @@ import ghidra.util.table.ReferencesFromTableModel; import ghidra.util.table.field.ReferenceEndpoint; /** - * A utility class to handle the generation of - * direct and offcut cross-reference (xref) lists + * A utility class to handle the generation of direct and offcut cross-reference (xref) lists * on code units and stack variables. + * + * @deprecated deprecated for 10.1; removal for 10.3 or later */ +@Deprecated // Use XReferenceUtils instead public class XReferenceUtil { private final static Address[] EMPTY_ADDR_ARRAY = new Address[0]; private final static Reference[] EMPTY_REF_ARRAY = new Reference[0]; @@ -59,11 +61,11 @@ public class XReferenceUtil { /** * Returns an array containing the first maxNumber * direct xref addresses to the specified code unit. - * + * * @param cu the code unit to generate the xrefs * @param maxNumber max number of xrefs to get, * or -1 to get all references - * + * * @return array first maxNumber xrefs to the code unit */ public final static Address[] getXRefList(CodeUnit cu, int maxNumber) { @@ -71,9 +73,8 @@ public class XReferenceUtil { if (prog == null) { return EMPTY_ADDR_ARRAY; } - List
xrefList = new ArrayList
(); - //lookup the direct xrefs to the current code unit - // + List
xrefList = new ArrayList<>(); + // lookup the direct xrefs to the current code unit ReferenceIterator iter = prog.getReferenceManager().getReferencesTo(cu.getMinAddress()); while (iter.hasNext()) { Reference ref = iter.next(); @@ -91,11 +92,11 @@ public class XReferenceUtil { /** * Returns an array containing the first maxNumber * direct xref references to the specified code unit. - * + * * @param cu the code unit to generate the xrefs * @param maxNumber max number of xrefs to get, * or -1 to get all references - * + * * @return array first maxNumber xrefs to the code unit */ public final static Reference[] getXReferences(CodeUnit cu, int maxNumber) { @@ -103,7 +104,7 @@ public class XReferenceUtil { if (prog == null) { return EMPTY_REF_ARRAY; } - List xrefList = new ArrayList(); + List xrefList = new ArrayList<>(); //lookup the direct xrefs to the current code unit // ReferenceIterator iter = prog.getReferenceManager().getReferencesTo(cu.getMinAddress()); @@ -156,7 +157,7 @@ public class XReferenceUtil { if (prog == null) { return EMPTY_ADDR_ARRAY; } - List
offcutList = new ArrayList
(); + List
offcutList = new ArrayList<>(); // Lookup the offcut xrefs... // if (cu.getLength() > 1) { @@ -195,7 +196,7 @@ public class XReferenceUtil { if (prog == null) { return EMPTY_REF_ARRAY; } - List offcutList = new ArrayList(); + List offcutList = new ArrayList<>(); // Lookup the offcut xrefs... // if (cu.getLength() > 1) { @@ -227,6 +228,7 @@ public class XReferenceUtil { * @return count of all offcut xrefs to the code unit */ public static int getOffcutXRefCount(CodeUnit cu) { + Program prog = cu.getProgram(); if (prog == null) { return 0; @@ -300,7 +302,7 @@ public class XReferenceUtil { } /** - * Shows all xrefs to the given location in a new table. These xrefs are retrieved + * Shows all xrefs to the given location in a new table. These xrefs are retrieved * from the given supplier. Thus, it is up to the client to determine which xrefs to show. * * @param navigatable the navigatable used for navigation from the table @@ -322,7 +324,7 @@ public class XReferenceUtil { /** * Returns all xrefs to the given location. If in data, then xrefs to the specific data - * component will be returned. Otherwise, the code unit containing the address of the + * component will be returned. Otherwise, the code unit containing the address of the * given location will be used as the source of the xrefs. * * @param location the location for which to get xrefs diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtils.java new file mode 100644 index 0000000000..bbfb99207c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/XReferenceUtils.java @@ -0,0 +1,212 @@ +/* ### + * 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 ghidra.app.util; + +import java.util.*; + +import ghidra.app.nav.Navigatable; +import ghidra.app.plugin.core.table.TableComponentProvider; +import ghidra.app.util.query.TableService; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.*; +import ghidra.program.model.data.DataUtilities; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.util.ProgramLocation; +import ghidra.util.table.ReferencesFromTableModel; +import ghidra.util.table.field.ReferenceEndpoint; + +public class XReferenceUtils { + + // Methods in this class treat -1 as a key to return all references and + // not cap the result set. + private final static int ALL_REFS = -1; + + /** + * Returns an array containing the first max + * direct xref references to the specified code unit. + * + * @param cu the code unit to generate the xrefs + * @param max max number of xrefs to get, or -1 to get all references + * + * @return array first max xrefs to the code unit + */ + public final static List getXReferences(CodeUnit cu, int max) { + Program program = cu.getProgram(); + if (program == null) { + Collections.emptyList(); + } + + // lookup the direct xrefs to the current code unit + List xrefs = new ArrayList<>(); + Address minAddress = cu.getMinAddress(); + ReferenceIterator it = program.getReferenceManager().getReferencesTo(minAddress); + while (it.hasNext()) { + if (xrefs.size() - max == 0) { + break; + } + + Reference ref = it.next(); + xrefs.add(ref); + } + + // Check for thunk reference + Function func = program.getFunctionManager().getFunctionAt(minAddress); + if (func != null) { + Address[] thunkAddrs = func.getFunctionThunkAddresses(); + if (thunkAddrs != null) { + for (Address thunkAddr : thunkAddrs) { + xrefs.add(new ThunkReference(thunkAddr, func.getEntryPoint())); + } + } + } + return xrefs; + } + + /** + * Returns an array containing all offcut xref references to the specified code unit + * + * @param cu the code unit to generate the offcut xrefs + * @param max max number of offcut xrefs to get, or -1 to get all offcut references + * @return array of all offcut xrefs to the code unit + */ + public static List getOffcutXReferences(CodeUnit cu, int max) { + Program program = cu.getProgram(); + if (program == null) { + return Collections.emptyList(); + } + + if (cu.getLength() <= 1) { + return Collections.emptyList(); + } + + List offcuts = new ArrayList<>(); + ReferenceManager refMgr = program.getReferenceManager(); + AddressSet set = new AddressSet(cu.getMinAddress().add(1), cu.getMaxAddress()); + AddressIterator it = refMgr.getReferenceDestinationIterator(set, true); + while (it.hasNext()) { + Address addr = it.next(); + ReferenceIterator refIter = refMgr.getReferencesTo(addr); + while (refIter.hasNext()) { + if (offcuts.size() - max == 0) { + break; + } + + Reference ref = refIter.next(); + offcuts.add(ref); + } + } + + return offcuts; + } + + /** + * Populates the provided lists with the direct and offcut xrefs to the specified variable + * + * @param var variable to get references + * @param xrefs list to put direct references in + * @param offcuts list to put offcut references in + */ + public static void getVariableRefs(Variable var, List xrefs, + List offcuts) { + getVariableRefs(var, xrefs, offcuts, ALL_REFS); + } + + /** + * Populates the provided lists with the direct and offcut xrefs to the specified variable + * + * @param var variable to get references + * @param xrefs list to put direct references in + * @param offcuts list to put offcut references in + * @param max max number of xrefs to get, or -1 to get all references + */ + public static void getVariableRefs(Variable var, List xrefs, + List offcuts, int max) { + + Address addr = var.getMinAddress(); + if (addr == null) { + return; + } + + Program program = var.getFunction().getProgram(); + ReferenceManager refMgr = program.getReferenceManager(); + Reference[] refs = refMgr.getReferencesTo(var); + int total = 0; + for (Reference vref : refs) { + if (total++ - max == 0) { + break; + } + + if (addr.equals(vref.getToAddress())) { + xrefs.add(vref); + } + else { + offcuts.add(vref); + } + } + } + + /** + * Returns all xrefs to the given location. If in data, then xrefs to the specific data + * component will be returned. Otherwise, the code unit containing the address of the + * given location will be used as the source of the xrefs. + * + * @param location the location for which to get xrefs + * @return the xrefs + */ + public static Set getAllXrefs(ProgramLocation location) { + + CodeUnit cu = DataUtilities.getDataAtLocation(location); + if (cu == null) { + Address toAddress = location.getAddress(); + Listing listing = location.getProgram().getListing(); + cu = listing.getCodeUnitContaining(toAddress); + } + + if (cu == null) { + return Collections.emptySet(); + } + + List xrefs = getXReferences(cu, ALL_REFS); + List offcuts = getOffcutXReferences(cu, ALL_REFS); + + // Remove duplicates + Set set = new HashSet<>(); + set.addAll(xrefs); + set.addAll(offcuts); + return set; + } + + /** + * Shows all xrefs to the given location in a new table. + * + * @param navigatable the navigatable used for navigation from the table + * @param serviceProvider the service provider needed to wire navigation + * @param service the service needed to show the table + * @param location the location for which to find references + * @param xrefs the xrefs to show + */ + public static void showXrefs(Navigatable navigatable, ServiceProvider serviceProvider, + TableService service, ProgramLocation location, Collection xrefs) { + + ReferencesFromTableModel model = + new ReferencesFromTableModel(new ArrayList<>(xrefs), serviceProvider, + location.getProgram()); + TableComponentProvider provider = service.showTable( + "XRefs to " + location.getAddress().toString(), "XRefs", model, "XRefs", navigatable); + provider.installRemoveItemsAction(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/ReferenceLineDispenser.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/ReferenceLineDispenser.java index 7c5a0dac75..c44f3fe5fd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/ReferenceLineDispenser.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/ReferenceLineDispenser.java @@ -17,12 +17,11 @@ package ghidra.app.util.exporter; import java.util.*; -import ghidra.app.util.XReferenceUtil; -import ghidra.program.model.address.Address; +import ghidra.app.util.XReferenceUtils; +import ghidra.program.model.address.*; import ghidra.program.model.listing.*; import ghidra.program.model.mem.Memory; -import ghidra.program.model.symbol.Reference; -import ghidra.program.model.symbol.ReferenceManager; +import ghidra.program.model.symbol.*; class ReferenceLineDispenser extends AbstractLineDispenser { @@ -35,48 +34,46 @@ class ReferenceLineDispenser extends AbstractLineDispenser { private Memory memory; private ReferenceManager referenceManager; - private List lines = new ArrayList(); + private List lines = new ArrayList<>(); ReferenceLineDispenser() { } - ReferenceLineDispenser(boolean forwardRefs, CodeUnit cu, Program program, ProgramTextOptions options) { - this.memory = program.getMemory(); + ReferenceLineDispenser(boolean forwardRefs, CodeUnit cu, Program program, + ProgramTextOptions options) { + this.memory = program.getMemory(); this.referenceManager = program.getReferenceManager(); this.displayRefHeader = options.isShowReferenceHeaders(); this.prefix = options.getCommentPrefix(); this.header = (forwardRefs ? " FWD" : "XREF"); this.headerWidth = options.getRefHeaderWidth(); this.width = options.getRefWidth(); - this.fillAmount = options.getAddrWidth() - + options.getBytesWidth() - + options.getLabelWidth(); + this.fillAmount = + options.getAddrWidth() + options.getBytesWidth() + options.getLabelWidth(); this.isHTML = options.isHTML(); - Address [] refs = (forwardRefs ? getForwardRefs(cu) : XReferenceUtil.getXRefList(cu)); - Address [] offcuts = (forwardRefs ? EMPTY_ADDR_ARR : XReferenceUtil.getOffcutXRefList(cu)); + Address[] refs = (forwardRefs ? getForwardRefs(cu) : getXRefList(cu)); + Address[] offcuts = (forwardRefs ? EMPTY_ADDR_ARR : getOffcutXRefList(cu)); processRefs(cu.getMinAddress(), refs, offcuts); } ReferenceLineDispenser(Variable var, Program program, ProgramTextOptions options) { - this.memory = program.getMemory(); + this.memory = program.getMemory(); this.referenceManager = program.getReferenceManager(); this.displayRefHeader = options.isShowReferenceHeaders(); this.header = "XREF"; this.headerWidth = options.getRefHeaderWidth(); this.prefix = options.getCommentPrefix(); this.width = options.getStackVarXrefWidth(); - this.fillAmount = options.getStackVarPreNameWidth() - + options.getStackVarNameWidth() - + options.getStackVarDataTypeWidth() - + options.getStackVarOffsetWidth() - + options.getStackVarCommentWidth(); + this.fillAmount = options.getStackVarPreNameWidth() + options.getStackVarNameWidth() + + options.getStackVarDataTypeWidth() + options.getStackVarOffsetWidth() + + options.getStackVarCommentWidth(); this.isHTML = options.isHTML(); - List xrefs = new ArrayList(); - List offcuts = new ArrayList(); - XReferenceUtil.getVariableRefs(var, xrefs, offcuts); + List xrefs = new ArrayList<>(); + List offcuts = new ArrayList<>(); + XReferenceUtils.getVariableRefs(var, xrefs, offcuts); Address[] xrefAddr = extractFromAddr(xrefs); Address[] offcutsAddr = extractFromAddr(offcuts); @@ -84,9 +81,9 @@ class ReferenceLineDispenser extends AbstractLineDispenser { xrefAddr, offcutsAddr); } - private Address [] extractFromAddr(List refs) { - Address [] addrs = new Address[refs.size()]; - for (int i=0; i < addrs.length; i++) { + private Address[] extractFromAddr(List refs) { + Address[] addrs = new Address[refs.size()]; + for (int i = 0; i < addrs.length; i++) { addrs[i] = refs.get(i).getFromAddress(); } Arrays.sort(addrs); @@ -113,18 +110,18 @@ class ReferenceLineDispenser extends AbstractLineDispenser { //////////////////////////////////////////////////////////////////// - private Address [] getForwardRefs(CodeUnit cu) { + private Address[] getForwardRefs(CodeUnit cu) { boolean showRefs = false; Address cuAddr = cu.getMinAddress(); - Reference [] monRefs = cu.getMnemonicReferences(); + Reference[] monRefs = cu.getMnemonicReferences(); Reference primMonRef = referenceManager.getPrimaryReferenceFrom(cuAddr, CodeUnit.MNEMONIC); showRefs = (monRefs.length == 1 && primMonRef == null) || (monRefs.length > 1); if (!showRefs) { int opCount = cu.getNumOperands(); - for (int i = 0 ; i < opCount ; ++i) { - Reference [] opRefs = cu.getOperandReferences(i); + for (int i = 0; i < opCount; ++i) { + Reference[] opRefs = cu.getOperandReferences(i); if (opRefs.length > 1) { showRefs = true; break; @@ -136,9 +133,9 @@ class ReferenceLineDispenser extends AbstractLineDispenser { return EMPTY_ADDR_ARR; } - Reference [] mRefs = cu.getReferencesFrom(); - Address [] refs = new Address[mRefs.length]; - for (int i = 0 ; i < mRefs.length ; ++i) { + Reference[] mRefs = cu.getReferencesFrom(); + Address[] refs = new Address[mRefs.length]; + for (int i = 0; i < mRefs.length; ++i) { refs[i] = mRefs[i].getToAddress(); } Arrays.sort(refs); @@ -147,7 +144,7 @@ class ReferenceLineDispenser extends AbstractLineDispenser { //////////////////////////////////////////////////////////////////// - private void processRefs(Address addr, Address [] refs, Address [] offcuts) { + private void processRefs(Address addr, Address[] refs, Address[] offcuts) { if (width < 1) { return; } @@ -157,8 +154,8 @@ class ReferenceLineDispenser extends AbstractLineDispenser { StringBuffer buf = new StringBuffer(); - Address [] all = new Address[refs.length + offcuts.length]; - System.arraycopy( refs, 0, all, 0, refs.length); + Address[] all = new Address[refs.length + offcuts.length]; + System.arraycopy(refs, 0, all, 0, refs.length); System.arraycopy(offcuts, 0, all, refs.length, offcuts.length); if (displayRefHeader) { @@ -224,4 +221,51 @@ class ReferenceLineDispenser extends AbstractLineDispenser { buf.delete(0, buf.length()); } } + + public static Address[] getXRefList(CodeUnit cu) { + Program prog = cu.getProgram(); + if (prog == null) { + return new Address[0]; + } + List
xrefList = new ArrayList<>(); + //lookup the direct xrefs to the current code unit + // + ReferenceIterator iter = prog.getReferenceManager().getReferencesTo(cu.getMinAddress()); + while (iter.hasNext()) { + Reference ref = iter.next(); + xrefList.add(ref.getFromAddress()); + } + Address[] arr = new Address[xrefList.size()]; + xrefList.toArray(arr); + Arrays.sort(arr); + return arr; + } + + private static Address[] getOffcutXRefList(CodeUnit cu) { + Program prog = cu.getProgram(); + if (prog == null) { + return new Address[0]; + } + List
offcutList = new ArrayList<>(); + // Lookup the offcut xrefs... + // + if (cu.getLength() > 1) { + ReferenceManager refMgr = prog.getReferenceManager(); + AddressSet set = + new AddressSet(cu.getMinAddress().add(1), cu.getMaxAddress()); + AddressIterator iter = refMgr.getReferenceDestinationIterator(set, true); + while (iter.hasNext()) { + Address addr = iter.next(); + ReferenceIterator refIter = refMgr.getReferencesTo(addr); + while (refIter.hasNext()) { + Reference ref = refIter.next(); + offcutList.add(ref.getFromAddress()); + } + } + } + Address[] arr = new Address[offcutList.size()]; + offcutList.toArray(arr); + Arrays.sort(arr); + return arr; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/IndentField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/IndentField.java index ad9d09c1b6..080c574dfa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/IndentField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/IndentField.java @@ -15,18 +15,16 @@ */ package ghidra.app.util.viewer.field; -import ghidra.app.util.viewer.format.FieldFormatModel; -import ghidra.app.util.viewer.proxy.EmptyProxy; -import ghidra.app.util.viewer.proxy.ProxyObj; - import java.awt.*; import javax.swing.JComponent; import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; -import docking.widgets.fieldpanel.support.FieldLocation; -import docking.widgets.fieldpanel.support.RowColLocation; +import docking.widgets.fieldpanel.support.*; +import ghidra.app.util.viewer.format.FieldFormatModel; +import ghidra.app.util.viewer.proxy.EmptyProxy; +import ghidra.app.util.viewer.proxy.ProxyObj; /** * Field responsible for drawing +/- symbols when over an aggregate datatype that @@ -177,7 +175,8 @@ public class IndentField implements ListingField { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { g.setColor(Color.LIGHT_GRAY); // draw the vertical lines to the left of the data (these are shown when there are vertical @@ -228,6 +227,11 @@ public class IndentField implements ListingField { return true; } + @Override + public int getNumDataRows() { + return 1; + } + @Override public int getNumRows() { return 1; @@ -307,7 +311,7 @@ public class IndentField implements ListingField { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - return new RowColLocation(0, 0); + return new DefaultRowColLocation(); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingTextField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingTextField.java index 1ae6ee3b67..502041cf1b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingTextField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingTextField.java @@ -17,6 +17,8 @@ package ghidra.app.util.viewer.field; import java.awt.Graphics; import java.awt.Rectangle; +import java.util.Arrays; +import java.util.List; import javax.swing.JComponent; @@ -34,7 +36,7 @@ import ghidra.app.util.viewer.proxy.ProxyObj; */ public class ListingTextField implements ListingField, TextField { - private ProxyObj proxy; + private ProxyObj proxy; private FieldFactory factory; protected TextField field; @@ -47,9 +49,11 @@ public class ListingTextField implements ListingField, TextField { * @param startX the starting X position of the field * @param width the width of the field * @param provider the highlight provider. + * @return the text field. */ - public static ListingTextField createSingleLineTextField(FieldFactory factory, ProxyObj proxy, - FieldElement fieldElement, int startX, int width, HighlightProvider provider) { + public static ListingTextField createSingleLineTextField(FieldFactory factory, + ProxyObj proxy, FieldElement fieldElement, int startX, int width, + HighlightProvider provider) { HighlightFactory hlFactory = new FieldHighlightFactory(provider, factory.getClass(), proxy.getObject()); @@ -58,7 +62,7 @@ public class ListingTextField implements ListingField, TextField { } public static ListingTextField createSingleLineTextFieldWithReverseClipping( - AddressFieldFactory factory, ProxyObj proxy, FieldElement fieldElement, int startX, + AddressFieldFactory factory, ProxyObj proxy, FieldElement fieldElement, int startX, int width, HighlightProvider provider) { HighlightFactory hlFactory = new FieldHighlightFactory(provider, factory.getClass(), proxy.getObject()); @@ -67,7 +71,7 @@ public class ListingTextField implements ListingField, TextField { } /** - * Displays the given text, word-wrapping as needed to avoid clipping (up to the max number of + * Displays the given text, word-wrapping as needed to avoid clipping (up to the max number of * lines.) * @param factory the field factory that generated this field * @param proxy the object used to populate this field @@ -77,9 +81,10 @@ public class ListingTextField implements ListingField, TextField { * @param width the width of the field * @param maxLines the maxLines to display. * @param provider the highlight provider. + * @return the text field. */ - public static ListingTextField createWordWrappedTextField(FieldFactory factory, ProxyObj proxy, - FieldElement fieldElement, int startX, int width, int maxLines, + public static ListingTextField createWordWrappedTextField(FieldFactory factory, + ProxyObj proxy, FieldElement fieldElement, int startX, int width, int maxLines, HighlightProvider provider) { HighlightFactory hlFactory = @@ -100,14 +105,16 @@ public class ListingTextField implements ListingField, TextField { * @param width the width of the field * @param maxLines the maxLines to display. * @param provider the highlight provider. + * @return the text field. */ - public static ListingTextField createPackedTextField(FieldFactory factory, ProxyObj proxy, + public static ListingTextField createPackedTextField(FieldFactory factory, ProxyObj proxy, FieldElement[] textElements, int startX, int width, int maxLines, HighlightProvider provider) { HighlightFactory hlFactory = new FieldHighlightFactory(provider, factory.getClass(), proxy.getObject()); - TextField field = new FlowLayoutTextField(textElements, startX, width, maxLines, hlFactory); + List list = Arrays.asList(textElements); + TextField field = new FlowLayoutTextField(list, startX, width, maxLines, hlFactory); return new ListingTextField(factory, proxy, field); } @@ -118,22 +125,24 @@ public class ListingTextField implements ListingField, TextField { * @param textElements the array of elements for the field. * Each of these holds text, attributes and location information. * @param startX the starting X position of the field - * @param width the widht of the field + * @param width the width of the field * @param maxLines the maxLines to display. * @param provider the highlight provider + * @return the text field. */ - public static ListingTextField createMultilineTextField(FieldFactory factory, ProxyObj proxy, + public static ListingTextField createMultilineTextField(FieldFactory factory, ProxyObj proxy, FieldElement[] textElements, int startX, int width, int maxLines, HighlightProvider provider) { HighlightFactory hlFactory = new FieldHighlightFactory(provider, factory.getClass(), proxy.getObject()); + List list = Arrays.asList(textElements); TextField field = - new VerticalLayoutTextField(textElements, startX, width, maxLines, hlFactory); + new VerticalLayoutTextField(list, startX, width, maxLines, hlFactory); return new ListingTextField(factory, proxy, field); } - protected ListingTextField(FieldFactory factory, ProxyObj proxy, TextField field) { + protected ListingTextField(FieldFactory factory, ProxyObj proxy, TextField field) { this.factory = factory; this.proxy = proxy; this.field = field; @@ -186,7 +195,8 @@ public class ListingTextField implements ListingField, TextField { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { field.paint(c, g, context, clip, map, cursorLoc, rowHeight); } @@ -195,6 +205,11 @@ public class ListingTextField implements ListingField, TextField { return field.contains(x, y); } + @Override + public int getNumDataRows() { + return field.getNumDataRows(); + } + @Override public int getNumRows() { return field.getNumRows(); @@ -281,7 +296,7 @@ public class ListingTextField implements ListingField, TextField { } @Override - public ProxyObj getProxy() { + public ProxyObj getProxy() { if (proxy == null) { return EmptyProxy.EMPTY_PROXY; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/NamespacePropertyEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/NamespacePropertyEditor.java index dba11577c2..a94ad0f93c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/NamespacePropertyEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/NamespacePropertyEditor.java @@ -33,8 +33,8 @@ public class NamespacePropertyEditor extends PropertyEditorSupport implements Cu private static final String DISPLAY_LOCAL_NAMESPACE_LABEL = "Display Local Namespace"; private static final String DISPLAY_NON_LOCAL_NAMESPACE_LABEL = "Display Non-local Namespace"; - private static final String LOCAL_NAMESPACE_PREFIX_LABEL = "Local namespace prefix"; - private static final String DISPLAY_LIBRARY_IN_NAMESPACE_LABEL = "Display library in namespace"; + private static final String LOCAL_NAMESPACE_PREFIX_LABEL = "Local Namespace Prefix"; + private static final String DISPLAY_LIBRARY_IN_NAMESPACE_LABEL = "Display library in Namespace"; private static final String[] NAMES = { DISPLAY_LOCAL_NAMESPACE_LABEL, DISPLAY_NON_LOCAL_NAMESPACE_LABEL, @@ -94,7 +94,7 @@ public class NamespacePropertyEditor extends PropertyEditorSupport implements Cu showLocalCheckBox.addItemListener(e -> { boolean enabled = showLocalCheckBox.isSelected(); - // only enable the text field if we are showing namespaces AND we are + // only enable the text field if we are showing namespaces AND we are // overriding the display value localPrefixField.setEnabled(enabled && useLocalPrefixCheckBox.isSelected()); useLocalPrefixCheckBox.setEnabled(enabled); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseField.java index e284a9f899..bdbd1069a4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseField.java @@ -22,8 +22,7 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; -import docking.widgets.fieldpanel.support.FieldLocation; -import docking.widgets.fieldpanel.support.RowColLocation; +import docking.widgets.fieldpanel.support.*; import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.proxy.EmptyProxy; import ghidra.app.util.viewer.proxy.ProxyObj; @@ -153,15 +152,16 @@ public class OpenCloseField implements ListingField { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, int rowHeight) { - + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { + // center in the heightAbove area (negative, since 0 is the baseline of text, which is at - // the bottom of the heightAbove) + // the bottom of the heightAbove) int toggleHandleStartY = -((heightAbove / 2) + (toggleHandleSize / 2)); int toggleHandleStartX = startX + (indentLevel * fieldWidth) + insetSpace; // TODO: If we're in printing mode, trying to render these open/close images - // causes the JVM to bomb. We'd like to eventually figure out why but in + // causes the JVM to bomb. We'd like to eventually figure out why but in // the meantime we can safely comment this out and still generate an acceptable // image. // @@ -178,7 +178,7 @@ public class OpenCloseField implements ListingField { g.setColor(Color.LIGHT_GRAY); - // draw the vertical lines to the left of the toggle handle (these are shown when + // draw the vertical lines to the left of the toggle handle (these are shown when // there are vertical bars drawn for inset data) int fieldTopY = -heightAbove; int fieldBottomY = heightBelow; @@ -205,7 +205,7 @@ public class OpenCloseField implements ListingField { boolean lastAndClosed = isLast && !isOpen; if (!lastAndClosed) { - // extended vertical line below toggle handle + // extended vertical line below toggle handle int buttonBottomY = toggleHandleStartY + toggleHandleSize; g.drawLine(midpointX, buttonBottomY, midpointX, fieldBottomY); } @@ -231,6 +231,11 @@ public class OpenCloseField implements ListingField { return true; } + @Override + public int getNumDataRows() { + return 1; + } + @Override public int getNumRows() { return 1; @@ -310,7 +315,7 @@ public class OpenCloseField implements ListingField { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - return new RowColLocation(0, 0); + return new DefaultRowColLocation(); } @Override @@ -332,7 +337,7 @@ public class OpenCloseField implements ListingField { //================================================================================================== // Static Methods -//================================================================================================== +//================================================================================================== static int getOpenCloseHandleSize() { return openImage.getIconWidth(); 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 f28bd38910..04c478d956 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 @@ -141,26 +141,23 @@ public class PlateFieldFactory extends FieldFactory { } CodeUnit cu = (CodeUnit) proxy.getObject(); - List elementList = new ArrayList<>(10); + List elements = new ArrayList<>(10); boolean isClipped = false; String commentText = getCommentText(cu); if ((commentText == null) || (commentText.isEmpty())) { - generateDefaultPlate(elementList, cu); + generateDefaultPlate(elements, cu); } else { - isClipped = generateFormattedPlateComment(elementList, cu); + isClipped = generateFormattedPlateComment(elements, cu); } - addBlankLines(elementList, cu); + addBlankLines(elements, cu); - if (elementList.size() == 0) { + if (elements.size() == 0) { // no real or default comment return null; } - FieldElement[] fields = new FieldElement[elementList.size()]; - elementList.toArray(fields); - if (isNestedDataAtSameAddressAsParent(proxy)) { // This is data at the same address as the parent, which happens with the first // element in a structure. We do not want to the plate comment here, but only at the @@ -169,7 +166,7 @@ public class PlateFieldFactory extends FieldFactory { } PlateFieldTextField textField = - new PlateFieldTextField(fields, this, proxy, startX, width, commentText, isClipped); + new PlateFieldTextField(elements, this, proxy, startX, width, commentText, isClipped); return new PlateListingTextField(proxy, textField); } @@ -706,7 +703,7 @@ public class PlateFieldFactory extends FieldFactory { private boolean isCommentClipped; private String commentText; - public PlateFieldTextField(FieldElement[] textElements, PlateFieldFactory factory, + public PlateFieldTextField(List textElements, PlateFieldFactory factory, ProxyObj proxy, int startX, int width, String commentText, boolean isCommentClipped) { super(textElements, startX, width, Integer.MAX_VALUE, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldFactory.java index ba9bb47dac..0bfea408ec 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldFactory.java @@ -23,7 +23,7 @@ import docking.widgets.fieldpanel.field.*; import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.fieldpanel.support.RowColLocation; import ghidra.app.util.HighlightProvider; -import ghidra.app.util.XReferenceUtil; +import ghidra.app.util.XReferenceUtils; import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.framework.options.Options; @@ -36,8 +36,6 @@ import ghidra.program.util.VariableXRefFieldLocation; /** * Variable Cross-reference Field Factory - *
- * */ public class VariableXRefFieldFactory extends XRefFieldFactory { @@ -76,9 +74,6 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { initDisplayOptions(); } - /** - * @see ghidra.app.util.viewer.field.FieldFactory#getField(ProxyObj, int) - */ @Override public ListingField getField(ProxyObj proxy, int varWidth) { Object obj = proxy.getObject(); @@ -89,7 +84,7 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { Variable var = (Variable) obj; List xrefs = new ArrayList<>(); List offcuts = new ArrayList<>(); - XReferenceUtil.getVariableRefs(var, xrefs, offcuts); + XReferenceUtils.getVariableRefs(var, xrefs, offcuts, maxXRefs); if (xrefs.size() + offcuts.size() == 0) { return null; @@ -164,9 +159,6 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { width, maxXRefs, hlProvider); } - /** - * @see ghidra.app.util.viewer.field.FieldFactory#getFieldLocation(ghidra.app.util.viewer.field.ListingField, BigInteger, int, ghidra.program.util.ProgramLocation) - */ @Override public FieldLocation getFieldLocation(ListingField bf, BigInteger index, int fieldNum, ProgramLocation loc) { @@ -188,9 +180,6 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { return null; } - /** - * @see ghidra.app.util.viewer.field.FieldFactory#getProgramLocation(int, int, ghidra.app.util.viewer.field.ListingField) - */ @Override public ProgramLocation getProgramLocation(int row, int col, ListingField bf) { Object obj = bf.getProxy().getObject(); @@ -207,7 +196,7 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { Variable var = (Variable) obj; List xrefs = new ArrayList<>(); List offcuts = new ArrayList<>(); - XReferenceUtil.getVariableRefs(var, xrefs, offcuts); + XReferenceUtils.getVariableRefs(var, xrefs, offcuts, maxXRefs); Reference ref = null; if (index < xrefs.size()) { @@ -225,9 +214,6 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { return null; } - /** - * @see ghidra.app.util.viewer.field.FieldFactory#acceptsType(int, java.lang.Class) - */ @Override public boolean acceptsType(int category, Class proxyObjectClass) { if (!Variable.class.isAssignableFrom(proxyObjectClass)) { @@ -239,7 +225,7 @@ public class VariableXRefFieldFactory extends XRefFieldFactory { @Override public FieldFactory newInstance(FieldFormatModel formatModel, HighlightProvider provider, - ToolOptions displayOptions, ToolOptions fieldOptions) { - return new VariableXRefFieldFactory(formatModel, provider, displayOptions, fieldOptions); + ToolOptions options, ToolOptions fieldOptions) { + return new VariableXRefFieldFactory(formatModel, provider, options, fieldOptions); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldMouseHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldMouseHandler.java index 9b2252a9f0..52d121c1a5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldMouseHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableXRefFieldMouseHandler.java @@ -15,16 +15,18 @@ */ package ghidra.app.util.viewer.field; +import java.util.HashSet; import java.util.Set; import ghidra.app.nav.Navigatable; -import ghidra.app.util.XReferenceUtil; +import ghidra.app.util.XReferenceUtils; import ghidra.app.util.query.TableService; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Variable; import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceManager; import ghidra.program.util.*; /** @@ -79,7 +81,24 @@ public class VariableXRefFieldMouseHandler extends XRefFieldMouseHandler { VariableLocation variableLocation = (VariableLocation) location; Variable variable = variableLocation.getVariable(); - Set refs = XReferenceUtil.getVariableRefs(variable); - XReferenceUtil.showAllXrefs(navigatable, serviceProvider, service, location, refs); + Set refs = getVariableRefs(variable); + XReferenceUtils.showXrefs(navigatable, serviceProvider, service, location, refs); + } + + private Set getVariableRefs(Variable var) { + + Set results = new HashSet<>(); + Address addr = var.getMinAddress(); + if (addr == null) { + return results; + } + + Program program = var.getFunction().getProgram(); + ReferenceManager refMgr = program.getReferenceManager(); + Reference[] refs = refMgr.getReferencesTo(var); + for (Reference vref : refs) { + results.add(vref); + } + return results; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldFactory.java index a2fbdee7be..e8f3022c7d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldFactory.java @@ -16,18 +16,19 @@ package ghidra.app.util.viewer.field; import java.awt.Color; +import java.awt.FontMetrics; import java.beans.PropertyEditor; import java.math.BigInteger; -import java.util.Arrays; -import java.util.Comparator; +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Predicate; import javax.swing.event.ChangeListener; import docking.widgets.fieldpanel.field.*; -import docking.widgets.fieldpanel.support.FieldLocation; -import docking.widgets.fieldpanel.support.RowColLocation; +import docking.widgets.fieldpanel.support.*; import ghidra.app.util.HighlightProvider; -import ghidra.app.util.XReferenceUtil; +import ghidra.app.util.XReferenceUtils; import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.options.OptionsGui; import ghidra.app.util.viewer.proxy.ProxyObj; @@ -41,6 +42,7 @@ import ghidra.program.util.ProgramLocation; import ghidra.program.util.XRefFieldLocation; import ghidra.util.HelpLocation; import ghidra.util.exception.AssertException; +import util.CollectionUtils; /** * Cross-reference Field Factory @@ -58,17 +60,19 @@ public class XRefFieldFactory extends FieldFactory { protected SORT_CHOICE sortChoice = SORT_CHOICE.Address; private static final String GROUP_TITLE = "XREFs Field"; - private static final String DELIMITER_MSG = GROUP_TITLE + Options.DELIMITER + "Delimiter"; - private static final String MAX_XREFS_MSG = + private static final String DELIMITER_KEY = GROUP_TITLE + Options.DELIMITER + "Delimiter"; + static final String MAX_XREFS_KEY = GROUP_TITLE + Options.DELIMITER + "Maximum Number of XREFs to Display"; - private static final String DISPLAY_BLOCK_NAME_MSG = + private static final String DISPLAY_BLOCK_NAME_KEY = GROUP_TITLE + Options.DELIMITER + "Display Local Block"; - private static final String SORT_OPTION = - GROUP_TITLE + Options.DELIMITER + "Sort References By"; - private static final String DISPLAY_REFERENCE_TYPE_MSG = + private static final String SORT_OPTION_KEY = + GROUP_TITLE + Options.DELIMITER + "Sort References by"; + private static final String DISPLAY_REFERENCE_TYPE_KEY = GROUP_TITLE + Options.DELIMITER + "Display Reference Type"; - private final static String NAMESPACE_OPTIONS = + private final static String NAMESPACE_OPTIONS_KEY = GROUP_TITLE + Options.DELIMITER + "Display Namespace"; + static final String GROUP_BY_FUNCTION_KEY = + GROUP_TITLE + Options.DELIMITER + "Group by Function"; private PropertyEditor namespaceOptionsEditor = new NamespacePropertyEditor(); @@ -78,7 +82,7 @@ public class XRefFieldFactory extends FieldFactory { protected Color otherColor; protected String delim = DELIMITER; protected boolean displayBlockName; - + protected boolean groupByFunction; protected int maxXRefs = MAX_XREFS; protected boolean displayRefType = true; protected Comparator typeComparator; @@ -90,9 +94,6 @@ public class XRefFieldFactory extends FieldFactory { private BrowserCodeUnitFormat codeUnitFormat; private ChangeListener codeUnitFormatListener = e -> XRefFieldFactory.this.model.update(); - /** - * Constructor - */ public XRefFieldFactory() { this(FIELD_NAME); } @@ -128,15 +129,18 @@ public class XRefFieldFactory extends FieldFactory { super(name, model, hlProvider, displayOptions, fieldOptions); HelpLocation hl = new HelpLocation("CodeBrowserPlugin", "XREFs_Field"); - fieldOptions.registerOption(DELIMITER_MSG, DELIMITER, hl, + fieldOptions.registerOption(DELIMITER_KEY, DELIMITER, hl, "Delimiter string used for separating multiple xrefs."); - fieldOptions.registerOption(DISPLAY_BLOCK_NAME_MSG, false, hl, + fieldOptions.registerOption(DISPLAY_BLOCK_NAME_KEY, false, hl, "Prepends xref addresses with the " + "name of the memory block containing the xref address."); - fieldOptions.registerOption(MAX_XREFS_MSG, MAX_XREFS, hl, + fieldOptions.registerOption(MAX_XREFS_KEY, MAX_XREFS, hl, "Sets the maximum number of xrefs to display."); - fieldOptions.registerOption(DISPLAY_REFERENCE_TYPE_MSG, true, hl, "Appends xref type."); - fieldOptions.registerOption(SORT_OPTION, SORT_CHOICE.Address, hl, "How to sort the xrefs"); + fieldOptions.registerOption(DISPLAY_REFERENCE_TYPE_KEY, true, hl, "Appends xref type."); + fieldOptions.registerOption(SORT_OPTION_KEY, SORT_CHOICE.Address, hl, + "How to sort the xrefs"); + fieldOptions.registerOption(GROUP_BY_FUNCTION_KEY, false, hl, + "True signals to group all xrefs by the containing calling function."); offcutColor = displayOptions.getColor(OptionsGui.XREF_OFFCUT.getColorOptionName(), OptionsGui.XREF_OFFCUT.getDefaultColor()); @@ -154,12 +158,13 @@ public class XRefFieldFactory extends FieldFactory { return r1.getReferenceType().toString().compareTo(r2.getReferenceType().toString()); }; - delim = fieldOptions.getString(DELIMITER_MSG, DELIMITER); - displayBlockName = fieldOptions.getBoolean(DISPLAY_BLOCK_NAME_MSG, false); + delim = fieldOptions.getString(DELIMITER_KEY, DELIMITER); + displayBlockName = fieldOptions.getBoolean(DISPLAY_BLOCK_NAME_KEY, false); - maxXRefs = fieldOptions.getInt(MAX_XREFS_MSG, MAX_XREFS); - sortChoice = fieldOptions.getEnum(SORT_OPTION, SORT_CHOICE.Address); - displayRefType = fieldOptions.getBoolean(DISPLAY_REFERENCE_TYPE_MSG, true); + maxXRefs = fieldOptions.getInt(MAX_XREFS_KEY, MAX_XREFS); + sortChoice = fieldOptions.getEnum(SORT_OPTION_KEY, SORT_CHOICE.Address); + displayRefType = fieldOptions.getBoolean(DISPLAY_REFERENCE_TYPE_KEY, true); + groupByFunction = fieldOptions.getBoolean(GROUP_BY_FUNCTION_KEY, false); fieldOptions.getOptions(GROUP_TITLE).setOptionsHelpLocation(hl); @@ -172,15 +177,17 @@ public class XRefFieldFactory extends FieldFactory { private void setupNamespaceOptions(Options fieldOptions) { // we need to install a custom editor that allows us to edit a group of related options - fieldOptions.registerOption(NAMESPACE_OPTIONS, OptionType.CUSTOM_TYPE, + fieldOptions.registerOption(NAMESPACE_OPTIONS_KEY, OptionType.CUSTOM_TYPE, new NamespaceWrappedOption(), null, "Adjusts the XREFs Field namespace display", namespaceOptionsEditor); - CustomOption customOption = fieldOptions.getCustomOption(NAMESPACE_OPTIONS, null); - fieldOptions.getOptions(NAMESPACE_OPTIONS).setOptionsHelpLocation( - new HelpLocation("CodeBrowserPlugin", "XREFs_Field")); + CustomOption customOption = fieldOptions.getCustomOption(NAMESPACE_OPTIONS_KEY, null); + fieldOptions.getOptions(NAMESPACE_OPTIONS_KEY) + .setOptionsHelpLocation( + new HelpLocation("CodeBrowserPlugin", "XREFs_Field")); if (!(customOption instanceof NamespaceWrappedOption)) { throw new AssertException( - "Someone set an option for " + NAMESPACE_OPTIONS + " that is not the expected " + + "Someone set an option for " + NAMESPACE_OPTIONS_KEY + + " that is not the expected " + "ghidra.app.util.viewer.field.NamespaceWrappedOption type."); } @@ -222,33 +229,40 @@ public class XRefFieldFactory extends FieldFactory { public void fieldOptionsChanged(Options options, String optionName, Object oldValue, Object newValue) { super.fieldOptionsChanged(options, optionName, oldValue, newValue); - if (optionName.equals(DELIMITER_MSG)) { + if (optionName.equals(DELIMITER_KEY)) { delim = (String) newValue; model.update(); } - else if (optionName.equals(DISPLAY_BLOCK_NAME_MSG)) { - displayBlockName = ((Boolean) newValue).booleanValue(); + else if (optionName.equals(DISPLAY_BLOCK_NAME_KEY)) { + displayBlockName = (Boolean) newValue; + model.update(); } - else if (optionName.equals(DISPLAY_REFERENCE_TYPE_MSG)) { - displayRefType = ((Boolean) newValue).booleanValue(); + else if (optionName.equals(DISPLAY_REFERENCE_TYPE_KEY)) { + displayRefType = (Boolean) newValue; + model.update(); } - else if (optionName.equals(MAX_XREFS_MSG)) { + else if (optionName.equals(MAX_XREFS_KEY)) { setMaxSize(((Integer) newValue).intValue(), options); + model.update(); } - else if (optionName.equals(SORT_OPTION)) { + else if (optionName.equals(SORT_OPTION_KEY)) { sortChoice = (SORT_CHOICE) newValue; model.update(); } - else if (optionName.equals(NAMESPACE_OPTIONS)) { + else if (optionName.equals(NAMESPACE_OPTIONS_KEY)) { setupNamespaceOptions(options); model.update(); } + else if (optionName.equals(GROUP_BY_FUNCTION_KEY)) { + groupByFunction = (Boolean) newValue; + model.update(); + } } private void setMaxSize(int n, Options options) { if (n < 1) { n = 1; - options.setInt(MAX_XREFS_MSG, 1); + options.setInt(MAX_XREFS_KEY, 1); } maxXRefs = n; } @@ -265,87 +279,362 @@ public class XRefFieldFactory extends FieldFactory { return null; } - if (obj == null || !(obj instanceof CodeUnit)) { + if (!(obj instanceof CodeUnit)) { return null; } + CodeUnit cu = (CodeUnit) obj; - Program pgm = cu.getProgram(); - - Reference[] xrefs = XReferenceUtil.getXReferences(cu, maxXRefs + 1); - - int maxOffcuts = Math.max(0, maxXRefs - xrefs.length); - Reference[] offcuts = XReferenceUtil.getOffcutXReferences(cu, maxOffcuts); + List xrefs = XReferenceUtils.getXReferences(cu, maxXRefs + 1); + int maxOffcuts = Math.max(0, maxXRefs - xrefs.size()); + List offcuts = XReferenceUtils.getOffcutXReferences(cu, maxOffcuts); if (sortChoice == SORT_CHOICE.Address) { - Arrays.sort(xrefs); - Arrays.sort(offcuts); + xrefs.sort(null); + offcuts.sort(null); } else { - Arrays.sort(xrefs, typeComparator); - Arrays.sort(offcuts, typeComparator); + xrefs.sort(typeComparator); + offcuts.sort(typeComparator); } - int totalXrefs = xrefs.length + offcuts.length; + + if (groupByFunction) { + return getFieldByFunction(proxy, varWidth, xrefs, offcuts); + } + return getFieldByAddress(proxy, varWidth, xrefs, offcuts); + } + + /* + Create a series of fields: 1 row per function and the xrefs it contains and 1 wrapping + field for all xrefs not in any function. The wrapping field will go below the function + based xrefs. It will look something like this: + + foo1: 123, 223 + foo2: 323, 333 + 423, 433, 567, + 899, [more] + + The fields and elements created by this method have this structure: + + XrefListingField + + CompositeVerticalLayoutTextField + + 0+ ClippingTextField + CompositeFieldElement + XrefFieldEleent + XrefAttributedString + + 0+ FlowLayoutTextField + XrefFieldEleent + XrefAttributedString + + + */ + private ListingField getFieldByFunction(ProxyObj proxy, int varWidth, + List xrefs, List offcuts) { + + int totalXrefs = xrefs.size() + offcuts.size(); if (totalXrefs == 0) { return null; } boolean tooMany = totalXrefs > maxXRefs; - AttributedString delimiter = new AttributedString(delim, Color.BLACK, getMetrics()); - FieldElement[] elements = new FieldElement[tooMany ? maxXRefs + 1 : totalXrefs]; - Function currentFunction = - pgm.getFunctionManager().getFunctionContaining(cu.getMinAddress()); - int count = 0; - for (; count < xrefs.length && count < elements.length; count++) { - String prefix = getPrefix(pgm, xrefs[count], currentFunction); - String addressString = xrefs[count].getFromAddress().toString(prefix); - AttributedString as = new AttributedString(addressString, color, getMetrics()); - if (displayRefType) { - as = createRefTypeAttributedString(xrefs[count], as); - } - if (count < totalXrefs - 1) { - as = new CompositeAttributedString(new AttributedString[] { as, delimiter }); + Object obj = proxy.getObject(); + CodeUnit cu = (CodeUnit) obj; + Program program = cu.getProgram(); + FontMetrics metrics = getMetrics(); + FunctionManager functionManager = program.getFunctionManager(); + + // + // Bin all xrefs by containing function, which may be null + // + List noFunction = new ArrayList<>(); + TreeMap> xrefsByFunction = new TreeMap<>((f1, f2) -> { + return f1.getEntryPoint().compareTo(f2.getEntryPoint()); + }); + for (Reference ref : CollectionUtils.asIterable(xrefs, offcuts)) { + + Function function = functionManager.getFunctionContaining(ref.getFromAddress()); + if (function == null) { + noFunction.add(ref); } else { - // This added to prevent a situation where resizing field to a particular size, - // resulted in layout of references to be strange - char[] charSpaces = new char[delimiter.length()]; - Arrays.fill(charSpaces, ' '); - AttributedString spaces = - new AttributedString(new String(charSpaces), color, getMetrics()); - as = new CompositeAttributedString(new AttributedString[] { as, spaces }); + xrefsByFunction.computeIfAbsent(function, r -> new ArrayList<>()).add(ref); } - elements[count] = new TextFieldElement(as, count, 0); } - for (int i = 0; i < offcuts.length && count < elements.length; i++, count++) { - String prefix = getPrefix(pgm, offcuts[i], currentFunction); - String addressString = offcuts[i].getFromAddress().toString(prefix); - AttributedString as = new AttributedString(addressString, offcutColor, getMetrics()); - if (displayRefType) { - as = createRefTypeAttributedString(offcuts[i], as); - } - if (count < totalXrefs - 1) { - as = new CompositeAttributedString(new AttributedString[] { as, delimiter }); - } - else { - // This added to prevent a situation where resizing field to a particular size, - // resulted in layout of references to be strange - char[] charSpaces = new char[delimiter.length()]; - Arrays.fill(charSpaces, ' '); - AttributedString spaces = - new AttributedString(new String(charSpaces), offcutColor, getMetrics()); - as = new CompositeAttributedString(new AttributedString[] { as, spaces }); - } - elements[count] = new TextFieldElement(as, count, 0); - } + // + // Create the function rows + // + Set offcutSet = new HashSet<>(offcuts); + Predicate isOffcut = r -> offcutSet.contains(r); + HighlightFactory hlFactory = + new FieldHighlightFactory(hlProvider, getClass(), proxy.getObject()); + Function currentFunction = functionManager.getFunctionContaining(cu.getMinAddress()); + List functionRows = + createXrefRowsByFunction(program, currentFunction, xrefsByFunction, isOffcut, varWidth, + hlFactory); + // + // TODO maxXRefs makes sense when simply displaying xrefs. What does max mean when + // binning xrefs by function. Currently, we use the max as the max row count, but + // this may need to be changed and it may require a new tool option. + // + + int maxLines = maxXRefs; + int availableLines = maxLines - functionRows.size(); if (tooMany) { - AttributedString as = new AttributedString(MORE_XREFS_STRING, color, getMetrics()); - elements[elements.length - 1] = new TextFieldElement(as, count - 1, 0); + // save room for the "more" field at the end + availableLines -= 1; } - return ListingTextField.createPackedTextField(this, proxy, elements, startX + varWidth, - width, maxXRefs, hlProvider); + // + // Create the row for xrefs not in a function + // + + // + // Note: the objects we build here want the 'data' row as a parameter, not the screen row. + // Out screen rows are what we are building to display; a data row we are here + // defining to be a single xref. This is a somewhat arbitrary decision. + int dataRow = totalXrefs - noFunction.size(); + TextField noFunctionXrefsField = + createWrappingXrefRow(program, dataRow, noFunction, currentFunction, isOffcut, + availableLines, hlFactory); + + List allFields = new ArrayList<>(); + allFields.addAll(functionRows); + if (noFunctionXrefsField != null) { + allFields.add(noFunctionXrefsField); + } + + int newStartX = startX + varWidth; + if (tooMany) { + // add the [more] element + int lastRow = allFields.size() - 1; + AttributedString as = new AttributedString(MORE_XREFS_STRING, color, metrics); + TextFieldElement moreElement = new TextFieldElement(as, lastRow, 0); + ClippingTextField ctf = new ClippingTextField(newStartX, width, moreElement, hlFactory); + allFields.add(ctf); + } + + CompositeVerticalLayoutTextField compositefield = + new CompositeVerticalLayoutTextField(allFields, newStartX, width, maxXRefs, hlFactory); + return new XrefListingField(this, proxy, compositefield); + } + + private List createXrefRowsByFunction(Program program, Function currentFunction, + TreeMap> xrefsByFunction, Predicate isOffcut, + int varWidth, + HighlightFactory hlFactory) { + + FontMetrics metrics = getMetrics(); + AttributedString delimiter = new AttributedString(delim, Color.BLACK, metrics); + + int row = 0; + List elements = new ArrayList<>(); + Set>> entries = xrefsByFunction.entrySet(); + for (Entry> entry : entries) { + + // + // Example row: functionName: 1234(c), 1238(c) + // + + List refs = entry.getValue(); + Function fromFunction = entry.getKey(); + String functionName = fromFunction.getName(); + int refCount = refs.size(); + String sizeText = ": "; + if (refCount > 1) { + sizeText = "[" + refs.size() + "]: "; + } + String text = functionName + sizeText; + AttributedString nameString = + new AttributedString(text, color, metrics); + List rowElements = new ArrayList<>(); + Reference firstRef = refs.get(0); + XrefAttributedString xrefString = + new XrefAttributedString(firstRef, nameString); + rowElements.add(new XrefFieldElement(xrefString, row, 0)); + + // + // TODO how many xrefs to display per function? + // + int n = Math.min(10, refs.size()); + for (int i = 0; i < n; i++) { + + boolean isLast = i == n - 1; + Reference ref = refs.get(i); + String prefix = getMergedPrefix(program, ref, currentFunction, fromFunction); + XrefFieldElement element = + createFunctionElement(program, prefix, ref, row, isLast ? null : delimiter, + isOffcut.test(ref)); + rowElements.add(element); + } + + elements.add(new CompositeFieldElement(rowElements)); + + row++; + } + + int newStartX = startX + varWidth; + List textFields = new ArrayList<>(); + for (FieldElement element : elements) { + textFields.add(new ClippingTextField(newStartX, width, element, hlFactory)); + } + + return textFields; + } + + private TextField createWrappingXrefRow(Program program, int startRow, List xrefs, + Function currentFunction, Predicate isOffcut, int availableLines, + HighlightFactory hlFactory) { + + FontMetrics metrics = getMetrics(); + AttributedString delimiter = new AttributedString(delim, Color.BLACK, metrics); + int row = startRow; + List elements = new ArrayList<>(); + for (Reference ref : xrefs) { + + String prefix = getPrefix(program, ref, currentFunction, null); + XrefFieldElement element = + createReferenceElement(program, prefix, ref, row, delimiter, isOffcut.test(ref)); + elements.add(element); + row++; + } + + // add all elements to a field that will wrap as needed + if (!elements.isEmpty()) { + List fieldElements = toFieldElements(elements, false); + return new FlowLayoutTextField(fieldElements, startX, width, availableLines, hlFactory); + } + + return null; + } + + /* + Create a series of fields: 1 row per function and the xrefs it contains and 1 wrapping + field for all xrefs not in any function. The wrapping field will go below the function + based xrefs. It will look something like this: + + foo1:423, + foo1:433, + foo2:567, + 899, [more] + + The fields and elements created by this method have this structure: + + XrefListingField + 1+ FlowLayoutTextField + XrefFieldEleent + XrefAttributedString + + + */ + private ListingField getFieldByAddress(ProxyObj proxy, int varWidth, List xrefs, + List offcuts) { + + int totalXrefs = xrefs.size() + offcuts.size(); + if (totalXrefs == 0) { + return null; + } + + Object obj = proxy.getObject(); + CodeUnit cu = (CodeUnit) obj; + Program program = cu.getProgram(); + FontMetrics metrics = getMetrics(); + AttributedString delimiter = new AttributedString(delim, Color.BLACK, metrics); + + Set offcutSet = new HashSet<>(offcuts); + Predicate isOffcut = r -> offcutSet.contains(r); + + boolean tooMany = totalXrefs > maxXRefs; + List elements = new ArrayList<>(); + FunctionManager functionManager = program.getFunctionManager(); + Function currentFunction = functionManager.getFunctionContaining(cu.getMinAddress()); + int n = tooMany ? maxXRefs + 1 : totalXrefs; + int count = 0; + for (; count < xrefs.size() && count < n; count++) { + Reference ref = xrefs.get(count); + String prefix = getPrefix(program, ref, currentFunction); + elements.add( + createReferenceElement(program, prefix, ref, count, delimiter, isOffcut.test(ref))); + } + + for (int i = 0; i < offcuts.size() && count < n; i++, count++) { + Reference ref = offcuts.get(i); + String prefix = getPrefix(program, ref, currentFunction); + elements.add( + createReferenceElement(program, prefix, ref, count, delimiter, isOffcut.test(ref))); + } + + if (!tooMany) { + XrefFieldElement lastElement = elements.get(elements.size() - 1); + lastElement.hideDelimiter(); + } + + List fieldElements = toFieldElements(elements, tooMany); + return createPackedTextField(proxy, varWidth, fieldElements); + } + + // note: this method was inspired by ListingTextField.createPackedTextField() + private XrefListingField createPackedTextField(ProxyObj proxy, int varWidth, + List list) { + + // assumption: the given array has been limited to the maxXref size already + int n = list.size(); + HighlightFactory hlFactory = + new FieldHighlightFactory(hlProvider, getClass(), proxy.getObject()); + TextField field = + new FlowLayoutTextField(list, startX + varWidth, width, n, hlFactory); + return new XrefListingField(this, proxy, field); + } + + private List toFieldElements(List list, boolean showEllipses) { + + List fieldElements = new ArrayList<>(list); + if (showEllipses) { + // add the 'more' string + int lastRow = list.size() - 1; + AttributedString as = new AttributedString(MORE_XREFS_STRING, color, getMetrics()); + fieldElements.add(new TextFieldElement(as, lastRow, 0)); + } + return fieldElements; + } + + private XrefFieldElement createFunctionElement(Program program, String prefix, Reference ref, + int row, AttributedString delimiter, boolean isOffcut) { + + FontMetrics metrics = getMetrics(); + String addressString = ref.getFromAddress().toString(prefix); + Color refColor = isOffcut ? offcutColor : color; + AttributedString addressPart = new AttributedString(addressString, refColor, metrics); + if (displayRefType) { + addressPart = createRefTypeAttributedString(ref, addressPart); + } + + XrefAttributedString xrefString = + new XrefAttributedString(ref, addressPart, delimiter); + if (delimiter == null) { + xrefString.hideDelimiter(); + } + + return new XrefFieldElement(xrefString, row, 0); + } + + private XrefFieldElement createReferenceElement(Program program, String prefix, Reference ref, + int row, AttributedString delimiter, boolean isOffcut) { + + FontMetrics metrics = getMetrics(); + String addressString = ref.getFromAddress().toString(prefix); + Color refColor = isOffcut ? offcutColor : color; + AttributedString as = new AttributedString(addressString, refColor, metrics); + if (displayRefType) { + as = createRefTypeAttributedString(ref, as); + } + + XrefAttributedString xrefString = + new XrefAttributedString(ref, as, delimiter); + return new XrefFieldElement(xrefString, row, 0); } protected AttributedString createRefTypeAttributedString(Reference reference, @@ -378,8 +667,16 @@ public class XRefFieldFactory extends FieldFactory { } protected String getPrefix(Program program, Reference reference, Function currentFunction) { - String prefix = ""; + Address fromAddress = reference.getFromAddress(); + Function fromFunction = program.getListing().getFunctionContaining(fromAddress); + return getPrefix(program, reference, currentFunction, fromFunction); + } + + private String getMergedPrefix(Program program, Reference reference, Function currentFunction, + Function fromFunction) { + + String prefix = ""; Address fromAddress = reference.getFromAddress(); if (displayBlockName) { prefix = getBlockName(program, fromAddress) + ":"; @@ -389,15 +686,34 @@ public class XRefFieldFactory extends FieldFactory { return prefix; // no namespaces being shown } - Function refFunction = program.getListing().getFunctionContaining(fromAddress); - if (refFunction == null) { + boolean isLocal = Objects.equals(currentFunction, fromFunction); + if (isLocal && useLocalPrefixOverride) { + return prefix + localPrefixText; + } + return prefix; + } + + private String getPrefix(Program program, Reference reference, Function currentFunction, + Function fromFunction) { + + String prefix = ""; + Address fromAddress = reference.getFromAddress(); + if (displayBlockName) { + prefix = getBlockName(program, fromAddress) + ":"; + } + + if (!displayLocalNamespace && !displayNonLocalNamespace) { + return prefix; // no namespaces being shown + } + + if (fromFunction == null) { return prefix; } - boolean isLocal = refFunction.equals(currentFunction); + boolean isLocal = fromFunction.equals(currentFunction); if (!isLocal) { if (displayNonLocalNamespace) { - return prefix + refFunction.getName() + ":"; + return prefix + fromFunction.getName() + ":"; } return prefix; // this means different function, but not displaying other namespaces } @@ -412,7 +728,6 @@ public class XRefFieldFactory extends FieldFactory { return prefix + localPrefixText; } return prefix + currentFunction.getName() + ":"; - } private String getRefTypeDisplayString(Reference reference) { @@ -460,60 +775,45 @@ public class XRefFieldFactory extends FieldFactory { protected FieldLocation createFieldLocation(int xrefPos, int xrefIndex, ListingTextField field, BigInteger index, int fieldNum) { - RowColLocation loc = field.dataToScreenLocation(xrefIndex, xrefPos); - return new FieldLocation(index, fieldNum, loc.row(), loc.col()); } @Override - public ProgramLocation getProgramLocation(int row, int col, ListingField bf) { - Object obj = bf.getProxy().getObject(); + public ProgramLocation getProgramLocation(int row, int col, ListingField listingField) { + Object obj = listingField.getProxy().getObject(); if (obj == null || !(obj instanceof CodeUnit)) { return null; } - CodeUnit cu = (CodeUnit) obj; - ListingTextField field = (ListingTextField) getField(bf.getProxy(), 0); - if (field == null) { + if (!(listingField instanceof XrefListingField)) { return null; } + CodeUnit cu = (CodeUnit) obj; + + int[] cpath = null; + if (cu instanceof Data) { + cpath = ((Data) cu).getComponentPath(); + } + + XrefListingField field = (XrefListingField) listingField; + FieldElement element = field.getFieldElement(row, col); RowColLocation loc = field.screenToDataLocation(row, col); - int index = loc.row(); - Reference[] xrefs = XReferenceUtil.getXReferences(cu, maxXRefs + 1); - if (sortChoice == SORT_CHOICE.Address) { - Arrays.sort(xrefs); - } - else { - Arrays.sort(xrefs, typeComparator); - } - - Address refAddr = null; - if (index < xrefs.length) { - refAddr = xrefs[index].getFromAddress(); - } - else { - Reference[] offcuts = XReferenceUtil.getOffcutXReferences(cu, maxXRefs); - if (sortChoice == SORT_CHOICE.Address) { - Arrays.sort(offcuts); - } - else { - Arrays.sort(offcuts, typeComparator); - } - if (index < xrefs.length + offcuts.length) { - refAddr = offcuts[index - xrefs.length].getFromAddress(); - } - } - - if (refAddr != null) { - int[] cpath = null; - if (cu instanceof Data) { - cpath = ((Data) cu).getComponentPath(); - } - return new XRefFieldLocation(cu.getProgram(), cu.getMinAddress(), cpath, refAddr, index, + if (element instanceof XrefFieldElement) { + XrefFieldElement xrefElement = (XrefFieldElement) element; + Reference xref = xrefElement.getXref(); + Address refAddr = xref.getFromAddress(); + return new XRefFieldLocation(cu.getProgram(), cu.getMinAddress(), cpath, refAddr, row, loc.col()); } + + String text = element.getText(); + if (MORE_XREFS_STRING.equals(text)) { + return new XRefFieldLocation(cu.getProgram(), cu.getMinAddress(), cpath, null, row, + loc.col()); + } + return null; } @@ -561,4 +861,77 @@ public class XRefFieldFactory extends FieldFactory { ToolOptions toolOptions, ToolOptions fieldOptions) { return new XRefFieldFactory(formatModel, provider, toolOptions, fieldOptions); } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private class XrefAttributedString extends CompositeAttributedString { + + private AttributedString content; + private AttributedString delimiter; + private Reference xref; + + public XrefAttributedString(Reference xref, AttributedString content) { + super(content); + this.content = content; + this.xref = xref; + } + + public XrefAttributedString(Reference xref, AttributedString content, + AttributedString delimiter) { + super(content, delimiter); + this.content = content; + this.delimiter = delimiter; + this.xref = xref; + } + + void hideDelimiter() { + AttributedString source = delimiter; + if (source == null) { + source = content; + } + + int length = delimiter == null ? 1 : delimiter.length(); + + // Use spaces instead of an empty string; this added to prevent a situation where + // resizing field to a particular size, resulted in layout of references to be strange + char[] charSpaces = new char[length]; + Arrays.fill(charSpaces, ' '); + AttributedString spaces = + new AttributedString(new String(charSpaces), source.getColor(0), + source.getFontMetrics(0)); + attributedStrings[attributedStrings.length - 1] = spaces; + } + + Reference getXref() { + return xref; + } + } + + private class XrefFieldElement extends TextFieldElement { + + private XrefAttributedString xrefString; + + public XrefFieldElement(XrefAttributedString xrefString, int row, int column) { + super(xrefString, row, column); + this.xrefString = xrefString; + } + + void hideDelimiter() { + xrefString.hideDelimiter(); + } + + Reference getXref() { + return xrefString.getXref(); + } + } + + private class XrefListingField extends ListingTextField { + + XrefListingField(XRefFieldFactory factory, ProxyObj proxy, TextField field) { + super(factory, proxy, field); + } + + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldMouseHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldMouseHandler.java index 96d0e78613..5f4b52f883 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldMouseHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefFieldMouseHandler.java @@ -22,7 +22,7 @@ import docking.widgets.fieldpanel.field.FieldElement; import docking.widgets.fieldpanel.field.TextField; import ghidra.app.nav.Navigatable; import ghidra.app.services.GoToService; -import ghidra.app.util.XReferenceUtil; +import ghidra.app.util.XReferenceUtils; import ghidra.app.util.query.TableService; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.address.Address; @@ -51,7 +51,7 @@ public class XRefFieldMouseHandler implements FieldMouseHandlerExtension { return false; } - // If I double-click on the XRef Header, show references to this place, also works on + // If I double-click on the XRef Header, show references to this place, also works on // 'more' field. This is much nicer if you have multiple references to navigate. if (isXREFHeaderLocation(location)) { showXRefDialog(sourceNavigatable, location, serviceProvider); @@ -105,8 +105,8 @@ public class XRefFieldMouseHandler implements FieldMouseHandlerExtension { return; } - Set refs = XReferenceUtil.getAllXrefs(location); - XReferenceUtil.showAllXrefs(navigatable, serviceProvider, service, location, refs); + Set refs = XReferenceUtils.getAllXrefs(location); + XReferenceUtils.showXrefs(navigatable, serviceProvider, service, location, refs); } protected ProgramLocation getReferredToLocation(Navigatable sourceNavigatable, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java index 4d587296b8..1c2d6140b9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/XRefHeaderFieldFactory.java @@ -16,17 +16,19 @@ package ghidra.app.util.viewer.field; import java.math.BigInteger; +import java.util.List; import docking.widgets.fieldpanel.field.*; import docking.widgets.fieldpanel.support.FieldLocation; import ghidra.app.util.HighlightProvider; -import ghidra.app.util.XReferenceUtil; +import ghidra.app.util.XReferenceUtils; import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.framework.options.Options; import ghidra.framework.options.ToolOptions; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Reference; import ghidra.program.util.ProgramLocation; import ghidra.program.util.XRefHeaderFieldLocation; @@ -126,14 +128,20 @@ public class XRefHeaderFieldFactory extends XRefFieldFactory { return null; } Program prog = cu.getProgram(); - int xrefCnt = prog.getReferenceManager().getReferenceCountTo(cu.getMinAddress()); - int offcutCnt = XReferenceUtil.getOffcutXRefCount(cu); + int xrefCount = prog.getReferenceManager().getReferenceCountTo(cu.getMinAddress()); + List offcuts = XReferenceUtils.getOffcutXReferences(cu, maxXRefs); + int offcutCount = offcuts.size(); - if (offcutCnt > 0) { - return "XREF[" + xrefCnt + "," + offcutCnt + "]: "; + if (offcutCount > 0) { + String modifier = ""; + if (offcutCount == maxXRefs) { + modifier = "+"; + } + return "XREF[" + xrefCount + "," + offcutCount + modifier + "]: "; } - if (xrefCnt > 0) { - return "XREF[" + xrefCnt + "]: "; + + if (xrefCount > 0) { + return "XREF[" + xrefCount + "]: "; } return null; } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java index 5f382d96d5..1a9b61d77d 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java @@ -41,10 +41,6 @@ public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationT private Options fieldOptions; private Program program; - public EolCommentFieldFactoryTest() { - super(); - } - @Before public void setUp() throws Exception { @@ -87,7 +83,7 @@ public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationT //================================================================================================== private ProgramDB buildProgram() throws Exception { - ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY, this); + ProgramBuilder builder = new ProgramBuilder("sample", ProgramBuilder._TOY, this); builder.createMemory(".text", "0x1001000", 0x6600); builder.createEmptyFunction(null, "0x1002000", 20, null); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/XRefFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/XRefFieldFactoryTest.java new file mode 100644 index 0000000000..97d6b7b97f --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/XRefFieldFactoryTest.java @@ -0,0 +1,623 @@ +/* ### + * 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 ghidra.app.util.viewer.field; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.*; + +import docking.widgets.fieldpanel.field.FieldElement; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.plugin.core.table.TableComponentProvider; +import ghidra.framework.options.Options; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceManager; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; +import ghidra.util.table.GhidraProgramTableModel; + +public class XRefFieldFactoryTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private ProgramBuilder builder; + private Program program; + + private CodeBrowserPlugin cb; + private Options fieldOptions; + + private int callerCount; + private int functionWithNoCalls; + private int functionCalledByOneOtherFunction; + private int functionCalledByMultipleFunctions; + private int functionWithAllTypesOfCalls; + private int nonFunctionOffset; + + @Before + public void setUp() throws Exception { + + program = buildProgram(); + + env = new TestEnv(); + env.launchDefaultTool(program); + cb = env.getPlugin(CodeBrowserPlugin.class); + fieldOptions = cb.getFormatManager().getFieldOptions(); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + private ProgramDB buildProgram() throws Exception { + builder = new ProgramBuilder("test", ProgramBuilder._TOY, this); + builder.createMemory(".text", "0x0", 0x100000); + + /* + Create a few functions that call other functions + + Create some function calls outside of functions + */ + + int callerOffset = 0x20000; + Function caller1 = caller(callerOffset); + Function caller2 = caller(callerOffset + 1000); + Function caller3 = caller(callerOffset + 2000); + Function caller4 = caller(callerOffset + 3000); + Function caller5 = caller(callerOffset + 4000); + Function caller6 = caller(callerOffset + 5000); + + // function with no calls + functionWithNoCalls = 0x0000; + function(functionWithNoCalls); + + // function called by one function once + functionCalledByOneOtherFunction = 0x1000; + function(functionCalledByOneOtherFunction); + createCallerReference(functionCalledByOneOtherFunction, caller1, 1); + + // function called by multiple functions multiple times each + functionCalledByMultipleFunctions = 0x2000; + function(functionCalledByMultipleFunctions); + createCallerReference(functionCalledByMultipleFunctions, caller2, 3); + createCallerReference(functionCalledByMultipleFunctions, caller3, 5); + + // function called my multiple functions multiple times each and calls from not in functions + functionWithAllTypesOfCalls = 0x3000; + function(functionWithAllTypesOfCalls); + createCallerReference(functionWithAllTypesOfCalls, caller4, 2); + createCallerReference(functionWithAllTypesOfCalls, caller5, 5); + createCallerReference(functionWithAllTypesOfCalls, caller6, 3); + + nonFunctionOffset = 0x30000; + createNonFunctionReferences(functionWithAllTypesOfCalls, nonFunctionOffset, 10); + + return builder.getProgram(); + } + + @Test + public void testXrefs_DefaultView() { + + /* + XREF[20]: callerFunction4:00020bbc(c), + callerFunction4:00020bc0(c), + callerFunction5:00020fa4(c), + callerFunction5:00020fa8(c), + callerFunction5:00020fac(c), + callerFunction5:00020fb0(c), + callerFunction5:00020fb4(c), + callerFunction6:0002138c(c), + callerFunction6:00021390(c), + callerFunction6:00021394(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(false); + + goToXrefField(functionWithAllTypesOfCalls); + + ListingTextField tf = (ListingTextField) cb.getCurrentField(); + + assertContainsRow(tf, "callerFunction4:00020bbc(c)"); + assertContainsRow(tf, "00030004(c), 00030008(c),"); + } + + @Test + public void testXrefs_GroupByFunctionView_CallsFromInFunctionsOnly() { + + /* + XREF[8]: callerFunction2[3]: 000203ec(c), + callerFunction3[5]: 000207d4(c), + */ + + setGroupByFunctionOption(true); + + goToXrefField(functionCalledByMultipleFunctions); + + ListingTextField tf = (ListingTextField) cb.getCurrentField(); + assertEquals(2, tf.getNumRows()); + assertContainsRow(tf, "callerFunction2[3]: 000203ec(c)"); + assertContainsRow(tf, "allerFunction3[5]: 000207d4(c)"); + } + + @Test + public void testXrefs_GroupByFunctionView_CallsFromInFunctionsAndNotInFunctions() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[5]: 00020fa4(c), + callerFunction6[3]: 0002138c(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(true); + + goToXrefField(functionWithAllTypesOfCalls); + + ListingTextField tf = (ListingTextField) cb.getCurrentField(); + + assertContainsRow(tf, "callerFunction4[2]: 00020bbc(c)"); + assertContainsRow(tf, "callerFunction5[5]: 00020fa4(c)"); + assertContainsRow(tf, "00030004(c), 00030008(c),"); + } + + @Test + public void testXrefs_DefaultView_NoXrefs() { + + setGroupByFunctionOption(false); + + assertFalse(hasXrefField(functionWithNoCalls)); + } + + @Test + public void testXrefs_DefaultView_DoubleClickFunctionName() { + + /* + XREF[20]: callerFunction4:00020bbc(c), + callerFunction4:00020bc0(c), + callerFunction5:00020fa4(c), + callerFunction5:00020fa8(c), + callerFunction5:00020fac(c), + callerFunction5:00020fb0(c), + callerFunction5:00020fb4(c), + callerFunction6:0002138c(c), + callerFunction6:00021390(c), + callerFunction6:00021394(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(false); + + String callerFunction = "callerFunction4"; + goToXrefField(functionWithAllTypesOfCalls, callerFunction); + + doubleClick(); + + assertInFunction(callerFunction); + } + + @Test + public void testXrefs_GroupByFunctionView_DoubleClickFunctionName() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[5]: 00020fa4(c), + callerFunction6[3]: 0002138c(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(true); + + goToXrefField(functionWithAllTypesOfCalls); + + String callerFunction = "callerFunction6"; + goToXrefField(functionWithAllTypesOfCalls, callerFunction); + + doubleClick(); + + assertInFunction(callerFunction); + } + + @Test + public void testXrefs_GroupByFunctionView_DoubleClickAddressInFunction() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[5]: 00020fa4(c), + callerFunction6[3]: 0002138c(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(true); + + goToXrefField(functionWithAllTypesOfCalls); + + String callerFunction = "callerFunction5"; + goToXrefField(functionWithAllTypesOfCalls, "00020fa4"); + + doubleClick(); + + assertInFunction(callerFunction); + } + + @Test + public void testXrefs_DefaultView_DoubleClickAddressNotInFunction() { + + /* + XREF[20]: callerFunction4:00020bbc(c), + callerFunction4:00020bc0(c), + callerFunction5:00020fa4(c), + callerFunction5:00020fa8(c), + callerFunction5:00020fac(c), + callerFunction5:00020fb0(c), + callerFunction5:00020fb4(c), + callerFunction6:0002138c(c), + callerFunction6:00021390(c), + callerFunction6:00021394(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(false); + + goToXrefField(functionWithAllTypesOfCalls); + + String addressNotInFunction = "00030018"; + goToXrefField(functionWithAllTypesOfCalls, addressNotInFunction); + + doubleClick(); + + assertAtAddress(addressNotInFunction); + } + + @Test + public void testXrefs_GroupByFunctionView_DoubleClickAddressNotInFunction() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[5]: 00020fa4(c), + callerFunction6[3]: 0002138c(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(true); + + goToXrefField(functionWithAllTypesOfCalls); + + String addressNotInFunction = "00030018"; + goToXrefField(functionWithAllTypesOfCalls, addressNotInFunction); + + doubleClick(); + + assertAtAddress(addressNotInFunction); + } + + @Test + public void testXrefs_DefaultView_DoubleClickToShowAllXrefs() { + + /* + XREF[20]: callerFunction4:00020bbc(c), + callerFunction4:00020bc0(c), + callerFunction5:00020fa4(c), + callerFunction5:00020fa8(c), + callerFunction5:00020fac(c), + callerFunction5:00020fb0(c), + callerFunction5:00020fb4(c), + callerFunction6:0002138c(c), + callerFunction6:00021390(c), + callerFunction6:00021394(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(false); + + goToXrefHeaderField(functionWithAllTypesOfCalls); + + doubleClick(); + + assertTableShowing(); + } + + @Test + public void testXrefs_GroupByFunctionView_DoubleClickToShowAllXrefs() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[5]: 00020fa4(c), + callerFunction6[3]: 0002138c(c), + 00030004(c), 00030008(c), + 0003000c(c), 00030010(c), + 00030014(c), 00030018(c), + 0003001c(c), 00030020(c), + 00030024(c), 00030028(c) + */ + + setGroupByFunctionOption(true); + + goToXrefHeaderField(functionWithAllTypesOfCalls); + + doubleClick(); + + assertTableShowing(); + } + + @Test + public void testXrefs_DefaultView_DoubleClick_More_Text() { + + /* + XREF[20]: callerFunction4:00020bbc(c), + callerFunction4:00020bc0(c), + callerFunction5:00020fa4(c), + callerFunction5:00020fa8(c), + callerFunction5:00020fac(c), + callerFunction5:00020fb0(c), + [more] + */ + + setGroupByFunctionOption(false); + setMaxXrefs(5); + + goToXrefField(functionWithAllTypesOfCalls, "more"); + + doubleClick(); + + assertTableShowing(); + } + + @Test + public void testXrefs_GroupByFunctionView_DoubleClick_More_Text() { + + /* + XREF[20]: callerFunction4[2]: 00020bbc(c), + callerFunction5[4]: 00020fa4(c), + [more] + */ + + setGroupByFunctionOption(true); + setMaxXrefs(5); + + goToXrefField(functionWithAllTypesOfCalls, "more"); + + doubleClick(); + + assertTableShowing(); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void assertTableShowing() { + TableComponentProvider table = waitForComponentProvider(TableComponentProvider.class); + GhidraProgramTableModel model = table.getModel(); + waitForCondition(() -> model.getRowCount() > 0); + } + + private void assertAtAddress(String expected) { + Address actual = cb.getCurrentAddress(); + assertEquals(expected, actual.toString()); + } + + private void assertInFunction(String text) { + + Address addr = cb.getCurrentAddress(); + FunctionManager functionManager = program.getFunctionManager(); + Function function = functionManager.getFunctionContaining(addr); + assertEquals(text, function.getName()); + } + + private void doubleClick() { + click(cb, 2, true); + } + + private void assertContainsRow(ListingTextField tf, String text) { + assertTrue("Expected '" + tf.getText() + "' to contain '" + text + "'", + tf.getText().contains(text)); + } + + private void goToXrefField(int addrOffset) { + assertTrue("Unable to navigate to xref field at " + Long.toHexString(addrOffset), + cb.goToField(addr(addrOffset), XRefFieldFactory.FIELD_NAME, 1, 1)); + } + + private void goToXrefHeaderField(int addrOffset) { + assertTrue("Unable to navigate to xref header field at " + Long.toHexString(addrOffset), + cb.goToField(addr(addrOffset), XRefHeaderFieldFactory.XREF_FIELD_NAME, 1, 1)); + } + + private void goToXrefField(int addrOffset, String text) { + + // is there a better way to find a field when given an address and some text? + + assertTrue("Unable to navigate to xref field at " + Long.toHexString(addrOffset), + cb.goToField(addr(addrOffset), XRefFieldFactory.FIELD_NAME, 1, 1)); + + ListingTextField tf = (ListingTextField) cb.getCurrentField(); + int rows = tf.getNumRows(); + for (int row = 0; row < rows; row++) { + + String rowText = getRowText(tf, row); + int col = rowText.indexOf(text); + if (col >= 0) { + col++; // move past the start position to ensure we are inside of the field + assertTrue("Unable to navigate to xref field at " + Long.toHexString(addrOffset), + cb.goToField(addr(addrOffset), XRefFieldFactory.FIELD_NAME, row, col)); + return; + } + } + + fail("Uanble to find text at " + Long.toHexString(addrOffset) + "; text: '" + text + "'"); + } + + private String getRowText(ListingTextField tf, int row) { + + List rowElements = getRowElements(tf, row); + return StringUtils.join(rowElements); + } + + private List getRowElements(ListingTextField tf, int row) { + + List elements = new ArrayList<>(); + int cols = tf.getNumCols(row); + for (int col = 0; col < cols; col++) { + FieldElement element = tf.getFieldElement(row, col); + if (!elements.contains(element)) { + elements.add(element); + } + } + return elements; + } + + private boolean hasXrefField(int addrOffset) { + return cb.goToField(addr(addrOffset), XRefFieldFactory.FIELD_NAME, 1, 1); + } + + private Function function(int addr) throws Exception { + return ensureFunction(addr); + } + + private Function caller(int addr) throws Exception { + String name = "callerFunction" + (++callerCount); + return ensureFunction(addr, name); + } + + private Function ensureFunction(long from) throws Exception { + ProgramDB p = builder.getProgram(); + FunctionManager fm = p.getFunctionManager(); + Function f = fm.getFunctionAt(addr(from)); + if (f != null) { + return f; + } + + String a = Long.toHexString(from); + return ensureFunction(from, "Function_" + a); + } + + private Function ensureFunction(long from, String name) throws Exception { + ProgramDB p = builder.getProgram(); + FunctionManager fm = p.getFunctionManager(); + Function f = fm.getFunctionAt(addr(from)); + if (f != null) { + return f; + } + + String a = Long.toHexString(from); + return builder.createEmptyFunction(name, "0x" + a, 500, DataType.DEFAULT); + } + + // creates n references from within caller to the given address + private void createCallerReference(int toAddr, Function caller, int n) { + int addr = (int) caller.getEntryPoint().getOffset(); + createMemoryReferencesReference(toAddr, addr, n); + } + + // create call reference to the given address + private void createNonFunctionReferences(int toAddr, int fromAddrRangeStart, int n) { + createMemoryReferencesReference(toAddr, fromAddrRangeStart, n); + } + + private void createMemoryReferencesReference(int toAddr, int fromAddrRangeStart, int n) { + + int offset = 4; + int addr = fromAddrRangeStart; + for (int i = 0; i < n; i++) { + addr += offset; + createReference(addr, toAddr); + } + } + + private boolean createReference(long from, long to) { + ProgramDB p = builder.getProgram(); + ReferenceManager rm = p.getReferenceManager(); + Reference existing = rm.getReference(addr(from), addr(to), 0); + if (existing != null) { + return false; + } + + builder.createMemoryCallReference("0x" + Long.toHexString(from), + "0x" + Long.toHexString(to)); + return true; + } + + private Address addr(long addr) { + return builder.addr(addr); + } + + private void setGroupByFunctionOption(boolean b) { + setBooleanOption(XRefFieldFactory.GROUP_BY_FUNCTION_KEY, b); + } + + private void setMaxXrefs(int n) { + setIntOptions(XRefFieldFactory.MAX_XREFS_KEY, n); + } + + private void setBooleanOption(String name, boolean value) { + + assertTrue("No such option '" + name + "'", fieldOptions.contains(name)); + + runSwing(() -> fieldOptions.setBoolean(name, value)); + waitForSwing(); + cb.updateNow(); + } + + private void setIntOptions(String name, int value) { + assertTrue("No such option '" + name + "'", fieldOptions.contains(name)); + + runSwing(() -> fieldOptions.setInt(name, value)); + waitForSwing(); + cb.updateNow(); + } + +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangTextField.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangTextField.java index cf04541e90..bacb407a59 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangTextField.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangTextField.java @@ -33,7 +33,7 @@ public class ClangTextField extends WrappingVerticalLayoutTextField { private FieldElement lineNumberFieldElement; private static FieldElement createSingleLineElement(FieldElement[] textElements) { - return new CompositeFieldElement(textElements, 0, textElements.length); + return new CompositeFieldElement(textElements); } /** diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerClipboardProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerClipboardProvider.java index c7d7f09ff3..015dcaa50e 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerClipboardProvider.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerClipboardProvider.java @@ -48,7 +48,7 @@ public class DecompilerClipboardProvider extends ByteCopier private static final PaintContext PAINT_CONTEXT = new PaintContext(); private static final ClipboardType TEXT_TYPE = new ClipboardType(DataFlavor.stringFlavor, "Text"); - private static final List COPY_TYPES = new LinkedList(); + private static final List COPY_TYPES = new LinkedList<>(); static { COPY_TYPES.add(TEXT_TYPE); @@ -58,7 +58,7 @@ public class DecompilerClipboardProvider extends ByteCopier private FieldSelection selection; private boolean copyFromSelectionEnabled; - private Set listeners = new CopyOnWriteArraySet(); + private Set listeners = new CopyOnWriteArraySet<>(); private int spaceCharWidthInPixels = 7; public DecompilerClipboardProvider(DecompilePlugin plugin, DecompilerProvider provider) { @@ -161,12 +161,12 @@ public class DecompilerClipboardProvider extends ByteCopier return false; } - protected Transferable copyText(TaskMonitor monitor) { + private Transferable copyText(TaskMonitor monitor) { return createStringTransferable(getText()); } - String getText() { - StringBuffer buffer = new StringBuffer(); + private String getText() { + StringBuilder buffer = new StringBuilder(); int numRanges = selection.getNumRanges(); for (int i = 0; i < numRanges; i++) { appendText(buffer, selection.getFieldRange(i)); @@ -174,7 +174,7 @@ public class DecompilerClipboardProvider extends ByteCopier return buffer.toString(); } - void appendText(StringBuffer buffer, FieldRange fieldRange) { + private void appendText(StringBuilder buffer, FieldRange fieldRange) { int startIndex = fieldRange.getStart().getIndex().intValue(); int endIndex = fieldRange.getEnd().getIndex().intValue(); if (startIndex == endIndex) { // single line selection (don't include padding) @@ -189,7 +189,7 @@ public class DecompilerClipboardProvider extends ByteCopier } } - private void appendText(StringBuffer buffer, int lineNumber, + private void appendText(StringBuilder buffer, int lineNumber, FieldSelection singleLineSelection) { if (singleLineSelection.isEmpty()) { return; @@ -224,7 +224,7 @@ public class DecompilerClipboardProvider extends ByteCopier } } - private void appendTextSingleLine(StringBuffer buffer, int lineNumber, + private void appendTextSingleLine(StringBuilder buffer, int lineNumber, FieldSelection singleLineSelection) { if (singleLineSelection.isEmpty()) { return; @@ -249,7 +249,7 @@ public class DecompilerClipboardProvider extends ByteCopier //================================================================================================== // Unsupported Operations -//================================================================================================== +//================================================================================================== @Override public boolean enablePaste() { 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 e81cd91f5f..2969e64b4a 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 @@ -24,9 +24,9 @@ import docking.widgets.fieldpanel.support.RowColLocation; /** * An object that wraps a string and provides data that describes how to render - * that string. + * that string. *

- * This class was created as a place to house attributes of rendering that + * This class was created as a place to house attributes of rendering that * are not described by Java's Font object, like underlining. * * @@ -83,7 +83,7 @@ abstract public class AbstractTextFieldElement implements FieldElement { @Override public int getMaxCharactersForWidth(int width) { - return attributedString.getColumnPosition(width); + return attributedString.getCharPosition(width); } @Override @@ -112,7 +112,8 @@ abstract public class AbstractTextFieldElement implements FieldElement { @Override public RowColLocation getDataLocationForCharacterIndex(int characterIndex) { if (characterIndex < 0 || characterIndex > attributedString.getText().length()) { - throw new IllegalArgumentException("columnPosition is out of range: " + characterIndex); + throw new IllegalArgumentException("columnPosition is out of range: " + characterIndex + + "; range is [0," + attributedString.getText().length() + "]"); } return new RowColLocation(row, column + characterIndex); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AttributedString.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AttributedString.java index a6f3ccbfed..777741dc67 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AttributedString.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/AttributedString.java @@ -24,9 +24,9 @@ import docking.util.GraphicsUtils; /** * An object that wraps a string and provides data that describes how to render - * that string. + * that string. *

- * This class was created as a place to house attributes of rendering that + * This class was created as a place to house attributes of rendering that * are not described by Java's Font object, like underlining. * * @@ -136,11 +136,11 @@ public class AttributedString { return fontMetrics.getMaxDescent() + UNDERLINE_HEIGHT; } - public int getColumnPosition(int width) { + public int getCharPosition(int x) { int subWidth = getIconWidth(); for (int i = 0; i < text.length(); i++) { subWidth += fontMetrics.charWidth(text.charAt(i)); - if (subWidth > width) { + if (subWidth > x) { return i; } } 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 961048623b..b18e84ff03 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 @@ -106,8 +106,7 @@ public class ClippingTextField implements TextField { @Override public int getCol(int row, int x) { - int xPos = Math.max(x - startX, 0); // make x relative to this fields - // coordinate system. + int xPos = Math.max(x - startX, 0); // make x relative to this fields coordinate system return textElement.getMaxCharactersForWidth(xPos); } @@ -134,7 +133,13 @@ public class ClippingTextField implements TextField { } private int getNumCols() { - return textElement.length() + 1; // allow one column past the end of the text + // allow one column past the end of the text to allow the cursor to be placed after the text + return textElement.length() + 1; + } + + @Override + public int getNumDataRows() { + return 1; } @Override @@ -217,7 +222,8 @@ public class ClippingTextField implements TextField { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, + int rowHeight) { if (context.isPrinting()) { print(g, context); } @@ -329,14 +335,21 @@ public class ClippingTextField implements TextField { */ @Override public RowColLocation screenToDataLocation(int screenRow, int screenColumn) { - return textElement.getDataLocationForCharacterIndex(screenColumn); + return originalElement.getDataLocationForCharacterIndex(screenColumn); } @Override public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { int column = textElement.getCharacterIndexForDataLocation(dataRow, dataColumn); - return new RowColLocation(0, Math.max(column, 0)); + if (column < 0) { + // place at the end if past the end + if (dataColumn >= textElement.length()) { + return new DefaultRowColLocation(0, textElement.length()); + } + return new DefaultRowColLocation(); + } + return new RowColLocation(0, column); } private int findX(int col) { @@ -381,7 +394,8 @@ public class ClippingTextField implements TextField { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - return new RowColLocation(0, Math.min(textOffset, textElement.getText().length() - 1)); + // allow the max position to be just after the last character + return new RowColLocation(0, Math.min(textOffset, textElement.getText().length())); } @Override @@ -395,9 +409,6 @@ public class ClippingTextField implements TextField { @Override public FieldElement getFieldElement(int screenRow, int screenColumn) { -// TODO - this used to return the clipped value, which is not our clients wanted (at least one). If -// any odd navigation/tracking/action issues appear, then this could be the culprit. -// return textElement.getFieldElement(screenColumn); return originalElement.getFieldElement(screenColumn); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeAttributedString.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeAttributedString.java index 66e1f45f1a..90760c600d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeAttributedString.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeAttributedString.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +26,7 @@ import javax.swing.JComponent; public class CompositeAttributedString extends AttributedString { private String fullText; - private AttributedString[] attributedStrings; + protected AttributedString[] attributedStrings; private int heightAbove = -1; private int heightBelow = -1; @@ -35,7 +34,7 @@ public class CompositeAttributedString extends AttributedString { this(stringList.toArray(new AttributedString[stringList.size()])); } - public CompositeAttributedString(AttributedString[] attributedStrings) { + public CompositeAttributedString(AttributedString... attributedStrings) { this.attributedStrings = attributedStrings; } @@ -54,17 +53,17 @@ public class CompositeAttributedString extends AttributedString { } @Override - public int getColumnPosition(int width) { - int remainingWidth = width; + public int getCharPosition(int x) { + int remainingWidth = x; int totalCharacters = 0; - for (int i = 0; i < attributedStrings.length; i++) { - int nextWidth = attributedStrings[i].getStringWidth(); + for (AttributedString attributedString : attributedStrings) { + int nextWidth = attributedString.getStringWidth(); if (nextWidth >= remainingWidth) { - totalCharacters += attributedStrings[i].getColumnPosition(remainingWidth); + totalCharacters += attributedString.getCharPosition(remainingWidth); break; } remainingWidth -= nextWidth; - totalCharacters += attributedStrings[i].length(); + totalCharacters += attributedString.length(); } return totalCharacters; @@ -86,8 +85,8 @@ public class CompositeAttributedString extends AttributedString { public int getHeightAbove() { if (heightAbove < 0) { heightAbove = 0; - for (int i = 0; i < attributedStrings.length; i++) { - heightAbove = Math.max(heightAbove, attributedStrings[i].getHeightAbove()); + for (AttributedString attributedString : attributedStrings) { + heightAbove = Math.max(heightAbove, attributedString.getHeightAbove()); } } return heightAbove; @@ -97,23 +96,23 @@ public class CompositeAttributedString extends AttributedString { public int getHeightBelow() { if (heightBelow < 0) { heightBelow = 0; - for (int i = 0; i < attributedStrings.length; i++) { - heightBelow = Math.max(heightBelow, attributedStrings[i].getHeightBelow()); + for (AttributedString attributedString : attributedStrings) { + heightBelow = Math.max(heightBelow, attributedString.getHeightBelow()); } } return heightBelow; } - // ============================================================================================= - // font metrics methods - // ============================================================================================= +// ============================================================================================= +// font metrics methods +// ============================================================================================= @Override public int getStringWidth() { if (textWidth == -1) { textWidth = 0; - for (int i = 0; i < attributedStrings.length; i++) { - textWidth += attributedStrings[i].getStringWidth(); + for (AttributedString attributedString : attributedStrings) { + textWidth += attributedString.getStringWidth(); } } return textWidth; @@ -123,24 +122,24 @@ public class CompositeAttributedString extends AttributedString { public String getText() { if (fullText == null) { StringBuffer buffer = new StringBuffer(); - for (int i = 0; i < attributedStrings.length; i++) { - buffer.append(attributedStrings[i].getText()); + for (AttributedString attributedString : attributedStrings) { + buffer.append(attributedString.getText()); } fullText = buffer.toString(); } return fullText; } - // ============================================================================================= - // paint methods - // ============================================================================================= +// ============================================================================================= +// paint methods +// ============================================================================================= @Override public void paint(JComponent c, Graphics g, int x, int y) { int xPos = x; - for (int i = 0; i < attributedStrings.length; i++) { - attributedStrings[i].paint(c, g, xPos, y); - xPos += attributedStrings[i].getStringWidth(); + for (AttributedString attributedString : attributedStrings) { + attributedString.paint(c, g, xPos, y); + xPos += attributedString.getStringWidth(); } } 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 fbe60d9051..38c56c63a1 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 @@ -24,7 +24,7 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.support.RowColLocation; /** - * A FieldElement that is composed of other FieldElements. + * A FieldElement that is composed of other FieldElements. The elements are laid out horizontally. */ public class CompositeFieldElement implements FieldElement { @@ -34,19 +34,14 @@ public class CompositeFieldElement implements FieldElement { private int textWidth = -1; private String fullText; - public CompositeFieldElement(List stringList) { - this(stringList.toArray(new FieldElement[stringList.size()])); + public CompositeFieldElement(List elements) { + this(elements.toArray(new FieldElement[elements.size()])); } public CompositeFieldElement(FieldElement[] fieldElements) { this.fieldElements = fieldElements; } - public CompositeFieldElement(FieldElement[] elements, int start, int length) { - fieldElements = new FieldElement[length]; - System.arraycopy(elements, start, fieldElements, 0, length); - } - private IndexedOffset getIndexedOffsetForCharPosition(int charPosition) { int n = 0; for (int i = 0; i < fieldElements.length; i++) { @@ -114,7 +109,7 @@ public class CompositeFieldElement implements FieldElement { //================================================================================================== // FontMetrics methods -//================================================================================================== +//================================================================================================== @Override public int getStringWidth() { @@ -130,7 +125,7 @@ public class CompositeFieldElement implements FieldElement { @Override public String getText() { if (fullText == null) { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); for (FieldElement fieldElement : fieldElements) { buffer.append(fieldElement.getText()); } @@ -141,7 +136,7 @@ public class CompositeFieldElement implements FieldElement { //================================================================================================== // Paint methods -//================================================================================================== +//================================================================================================== @Override public void paint(JComponent c, Graphics g, int x, int y) { @@ -217,9 +212,14 @@ public class CompositeFieldElement implements FieldElement { return getText().length(); } + @Override + public String toString() { + return getText(); + } + //================================================================================================== // Location Info -//================================================================================================== +//================================================================================================== @Override public RowColLocation getDataLocationForCharacterIndex(int characterIndex) { @@ -229,12 +229,14 @@ public class CompositeFieldElement implements FieldElement { @Override public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) { - int columnCount = 0; + int columnsSoFar = 0; for (int i = fieldElements.length - 1; i >= 0; i--) { - columnCount += fieldElements[i].length(); + columnsSoFar += fieldElements[i].length(); int column = fieldElements[i].getCharacterIndexForDataLocation(dataRow, dataColumn); if (column != -1) { - return length() - columnCount + column; + // column value is relative to the current field; convert it to this field's offset + int fieldStart = length() - columnsSoFar; + return fieldStart + column; } } 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 new file mode 100644 index 0000000000..9bdd9ba668 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextField.java @@ -0,0 +1,596 @@ +/* ### + * 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.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.JComponent; + +import org.apache.commons.lang3.StringUtils; + +import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; +import docking.widgets.fieldpanel.internal.PaintContext; +import docking.widgets.fieldpanel.support.*; +import generic.json.Json; + +/** + * A {@link TextField} that takes in other TextFields. + * + *

This class allows clients to create custom text layout behavior by combining individual + * TextFields that dictate layout behavior. As an example, consider this rendering: + *

+ * 	1)  This is some text...
+ * 	2)	This
+ * 		is
+ * 		more
+ * 		text
+ * 
+ * In this example, 1) is a row of text inside of a {@link ClippingTextField}. Row 2) is a + * multi-line text rendering specified in a single {@link FlowLayoutTextField}, using a + * narrow width to trigger the field to place each element on its own line. + */ +public class CompositeVerticalLayoutTextField implements TextField { + + // the view rows, which may be a clipped version of the client fields + private List fieldRows; + private int startX; + private int width; + private int preferredWidth; + private HighlightFactory hlFactory; + + private int height; + private int heightAbove; + private int numRows; + private int numDataRows; + private boolean isPrimary; + + private String fullText; + + // all text, including any clipped text; lines.size() == fields.size() + private List lines; + + // used in the getText() method to separate rows without adding newlines + private String rowSeparator; + + private boolean isClipped; + + public CompositeVerticalLayoutTextField(List fields, int startX, int width, + int maxLines, HighlightFactory hlFactory) { + this(fields, startX, width, maxLines, hlFactory, " "); + } + + protected CompositeVerticalLayoutTextField(List fields, int startX, int width, + int maxLines, HighlightFactory hlFactory, String rowSeparator) { + + this.startX = startX; + this.width = width; + + this.hlFactory = hlFactory; + this.rowSeparator = rowSeparator; + + lines = generateLines(fields); + fullText = generateText(fields, rowSeparator); + + heightAbove = (fields.get(0)).getHeightAbove(); + fieldRows = layoutRows(fields, maxLines); + + calculateRows(fields); + calculatePreferredWidth(); + calculateHeight(); + } + + private List generateLines(List fields) { + + List list = new ArrayList<>(); + for (TextField field : fields) { + list.add(field.getTextWithLineSeparators()); + } + return list; + } + + private String generateText(List fields, String delimiter) { + + StringBuilder buf = new StringBuilder(); + for (TextField element : fields) { + buf.append(element.getText()).append(delimiter); + } + return buf.toString(); + } + + private List layoutRows(List fields, int maxLines) { + + List newSubFields = new ArrayList<>(); + int heightSoFar = -heightAbove; + int currentRow = 0; + boolean tooManyLines = fields.size() > maxLines; + for (int i = 0; i < fields.size() && i < maxLines; i++) { + TextField field = fields.get(i); + if (tooManyLines && (i == maxLines - 1)) { + FieldElement element = field.getFieldElement(0, 0); + TextField newField = createClippedField(element); + newSubFields.add(new FieldRow(newField, currentRow, heightSoFar)); + isClipped = true; + } + else { + newSubFields.add(new FieldRow(field, currentRow, heightSoFar)); + isClipped |= field.isClipped(); + } + + heightSoFar += field.getHeight(); + currentRow += field.getNumRows(); + } + + isClipped |= tooManyLines; + + return newSubFields; + } + + private ClippingTextField createClippedField(FieldElement element) { + + FieldElement[] elements = new FieldElement[] { + element, + new StrutFieldElement(500) + }; + FieldElement compositeElement = new CompositeFieldElement(elements); + return new ClippingTextField(startX, width, compositeElement, hlFactory); + } + + private void calculateHeight() { + for (FieldRow row : fieldRows) { + height += row.field.getHeight(); + } + } + + private void calculatePreferredWidth() { + preferredWidth = 0; + for (FieldRow row : fieldRows) { + preferredWidth = Math.max(preferredWidth, row.field.getPreferredWidth()); + } + } + + private void calculateRows(List fields) { + numRows = 0; + for (FieldRow row : fieldRows) { + numRows += row.field.getNumRows(); + } + + numDataRows = 0; + for (TextField field : fields) { + numDataRows += field.getNumDataRows(); + } + } + + @Override + public String toString() { + return getText(); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getPreferredWidth() { + return preferredWidth; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public int getStartX() { + return startX; + } + + @Override + public int getNumDataRows() { + return numDataRows; + } + + @Override + public int getNumRows() { + return numRows; + } + + @Override + public int getHeightAbove() { + return heightAbove; + } + + @Override + public int getHeightBelow() { + return height - heightAbove; + } + + @Override + public boolean isPrimary() { + return isPrimary; + } + + @Override + public void rowHeightChanged(int newHeightAbove, int newHeightBelow) { + // don't care + } + + @Override + public boolean isClipped() { + return isClipped; + } + + @Override + public void setPrimary(boolean state) { + isPrimary = state; + } + + @Override + public String getText() { + return fullText; + } + + @Override + public String getTextWithLineSeparators() { + return StringUtils.join(lines, '\n'); + } + + @Override + public void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip, + FieldBackgroundColorManager colorManager, RowColLocation cursorLocation, + int rowHeight) { + + // the graphics have been translated such that the first line of text's base line is + // at y=0 (So if we are not clipped, we will drawing from a negative value that is the + // font's height above the baseline (-heightAbove) to rowHeight (-heightAbove) + int myStartY = -heightAbove; + int myEndY = myStartY + rowHeight; + int clipStartY = clip.y; + int clipEndY = clip.y + clip.height; + + Color fieldBackgroundColor = colorManager.getBackgroundColor(); + if (fieldBackgroundColor != null) { + g.setColor(fieldBackgroundColor); + + // restrict background rectangle to clipping rectangle + int startY = Math.max(myStartY, clipStartY); + int endY = Math.min(myEndY, clipEndY); + int clippedHeight = endY - startY; + g.fillRect(startX, startY, width, clippedHeight); + } + + FieldRow cursorRow = null; + if (cursorLocation != null) { + cursorRow = getFieldRow(cursorLocation.row()); + } + + int startY = myStartY; + int translatedY = 0; + + for (int i = 0; i < fieldRows.size(); i++) { + + // if past clipping region we are done + if (startY > clipEndY) { + break; + } + + FieldRow fieldRow = fieldRows.get(i); + TextField field = fieldRow.field; + int subFieldHeight = fieldRow.field.getHeight(); + int endY = startY + subFieldHeight; + + // if any part of the line is in the clip region, draw it + if (endY >= clipStartY) { + RowColLocation cursor = null; + if (fieldRow == cursorRow) { + int relativeRow = fieldRow.getRelativeRow(cursorLocation.row()); + cursor = cursorLocation.withRow(relativeRow); + } + + field.paint(c, g, context, clip, colorManager, cursor, rowHeight); + } + + // translate for next row of text + startY += subFieldHeight; + g.translate(0, subFieldHeight); + translatedY += subFieldHeight; + } + + // restore the graphics to where it was when we started. + g.translate(0, -translatedY); + } + + @Override + public boolean contains(int x, int y) { + if ((x >= startX) && (x < startX + width) && (y >= -heightAbove) && + (y < height - heightAbove)) { + return true; + } + return false; + } + + public String getRowSeparator() { + return rowSeparator; + } + + private FieldRow getFieldRow(int screenRow) { + int currentRow = 0; + for (FieldRow row : fieldRows) { + int n = row.field.getNumRows(); + if (currentRow + n > screenRow) { + return row; + } + currentRow += n; + } + return fieldRows.get(fieldRows.size() - 1); + } + + private FieldRow getFieldRowFromDataRow(int dataRow) { + int currentRow = 0; + for (FieldRow row : fieldRows) { + if (currentRow >= dataRow) { + return row; + } + currentRow += row.field.getNumDataRows(); + } + return fieldRows.get(fieldRows.size() - 1); + } + + // get all rows from 0 to max inclusive + private List getAllRows(int maxRow) { + int currentRow = 0; + List list = new ArrayList<>(); + for (FieldRow row : fieldRows) { + if (currentRow > maxRow) { + break; + } + + list.add(row); + currentRow += row.field.getNumRows(); + } + return list; + } + + // for testing + protected List getAllRowsUpTo(int maxRowInclusive) { + return getAllRows(maxRowInclusive) + .stream() + .map(fieldRow -> fieldRow.field) + .collect(Collectors.toList()); + } + + @Override + public FieldElement getFieldElement(int screenRow, int screenColumn) { + FieldRow fieldRow = getFieldRow(screenRow); + int relativeRow = fieldRow.getRelativeRow(screenRow); + return fieldRow.field.getFieldElement(relativeRow, screenColumn); + } + + @Override + public int getNumCols(int row) { + FieldRow fieldRow = getFieldRow(row); + int relativeRow = fieldRow.getRelativeRow(row); + return fieldRow.field.getNumCols(relativeRow); + } + + @Override + public int getX(int row, int col) { + FieldRow fieldRow = getFieldRow(row); + int relativeRow = fieldRow.getRelativeRow(row); + return fieldRow.field.getX(relativeRow, col); + } + + @Override + public int getY(int row) { + + int y = -heightAbove; + List rows = getAllRows(row); + for (FieldRow fieldRow : rows) { + y += fieldRow.field.getHeight(); + } + return y; + } + + @Override + public int getRow(int y) { + if (y < 0) { + return 0; + } + + int heightSoFar = 0; + for (FieldRow fieldRow : fieldRows) { + int fieldHeight = fieldRow.field.getHeight(); + int bottom = fieldHeight + heightSoFar; + if (bottom > y) { + int relativeY = y - heightSoFar; + int relativeRow = fieldRow.field.getRow(relativeY); + int displayRow = fieldRow.fromRelativeRow(relativeRow); + return displayRow; + } + heightSoFar += fieldHeight; + } + return getNumRows() - 1; + } + + @Override + public int getCol(int row, int x) { + + FieldRow fieldRow = getFieldRow(row); + int relativeRow = fieldRow.getRelativeRow(row); + return fieldRow.field.getCol(relativeRow, x); + } + + @Override + public boolean isValid(int row, int col) { + + if ((row < 0) || (row >= getNumRows())) { + return false; + } + + FieldRow fieldRow = getFieldRow(row); + int relativeRow = fieldRow.getRelativeRow(row); + return fieldRow.field.isValid(relativeRow, col); + } + + @Override + public Rectangle getCursorBounds(int row, int col) { + + if ((row < 0) || (row >= getNumRows())) { + return null; + } + + List rows = getAllRows(row); + FieldRow cursorRow = rows.get(rows.size() - 1); + int relativeRow = cursorRow.getRelativeRow(row); + Rectangle r = cursorRow.field.getCursorBounds(relativeRow, col); + + for (int i = 0; i < rows.size() - 1; i++) { + FieldRow previousRow = rows.get(i); + r.y += previousRow.field.getHeight(); + } + return r; + } + + @Override + public int getScrollableUnitIncrement(int topOfScreen, int direction, int max) { + + if ((topOfScreen < -heightAbove) || (topOfScreen > height - heightAbove)) { + return max; + } + + int row = getRow(topOfScreen); + int y = getY(row); + int rowOffset = topOfScreen - y; + FieldRow fieldRow = getFieldRow(row); + int rowHeight = fieldRow.field.getHeight(); + if (direction > 0) { // if scrolling down + return rowHeight - rowOffset; + } + else if (rowOffset == 0) { + return -rowHeight; + } + else { + return -rowOffset; + } + } + + @Override + public RowColLocation screenToDataLocation(int screenRow, int screenColumn) { + + screenRow = Math.min(screenRow, numRows - 1); + screenRow = Math.max(screenRow, 0); + + FieldRow fieldRow = getFieldRow(screenRow); + + screenColumn = Math.min(screenColumn, fieldRow.field.getText().length()); + screenColumn = Math.max(screenColumn, 0); + + int relativeRow = fieldRow.getRelativeRow(screenRow); + return fieldRow.field.screenToDataLocation(relativeRow, screenColumn); + } + + @Override + public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { + FieldRow fieldRow = getFieldRowFromDataRow(dataRow); + RowColLocation location = fieldRow.field.dataToScreenLocation(dataRow, dataColumn); + int relativeRow = fieldRow.fromRelativeRow(location.row()); + return location.withRow(relativeRow); + } + + @Override + public int screenLocationToTextOffset(int row, int col) { + + if (row >= numRows) { + return getText().length(); + } + + int extraSpace = rowSeparator.length(); + int len = 0; + List rows = getAllRows(row); + int n = rows.size() - 1; + for (int i = 0; i < n; i++) { + FieldRow fieldRow = rows.get(i); + len += fieldRow.field.getText().length() + extraSpace; + } + + FieldRow lastRow = rows.get(n); + int relativeRow = lastRow.getRelativeRow(row); + len += lastRow.field.screenLocationToTextOffset(relativeRow, col); + return len; + } + + @Override + public RowColLocation textOffsetToScreenLocation(int textOffset) { + + int extraSpace = rowSeparator.length(); + int n = fieldRows.size(); + int textOffsetSoFar = 0; + for (int i = 0; i < n; i++) { + + if (textOffsetSoFar > textOffset) { + break; + } + + FieldRow fieldRow = fieldRows.get(i); + int length = fieldRow.field.getText().length() + extraSpace; + int end = textOffsetSoFar + length; + if (end > textOffset) { + int relativeOffset = textOffset - textOffsetSoFar; + RowColLocation location = fieldRow.field.textOffsetToScreenLocation(relativeOffset); + int screenRow = fieldRow.fromRelativeRow(location.row()); + return location.withRow(screenRow); + } + + textOffsetSoFar += length; + } + + FieldRow lastRow = fieldRows.get(fieldRows.size() - 1); + int length = lastRow.field.getText().length(); + return new DefaultRowColLocation(numRows - 1, length); + } + + private class FieldRow { + private TextField field; + private int displayRowOffset; + private int yOffset; + + FieldRow(TextField field, int rowOffset, int yOffset) { + this.field = field; + this.displayRowOffset = rowOffset; + } + + // used to turn given row into 0 for this composite field + int getRelativeRow(int displayRow) { + return displayRow - displayRowOffset; + } + + int fromRelativeRow(int relativeRow) { + 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/EmptyTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyTextField.java index 9d2f2ff651..0cbd444a36 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyTextField.java @@ -21,6 +21,7 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; +import docking.widgets.fieldpanel.support.DefaultRowColLocation; import docking.widgets.fieldpanel.support.RowColLocation; /** @@ -79,6 +80,11 @@ public class EmptyTextField implements Field { return startX; } + @Override + public int getNumDataRows() { + return 1; + } + @Override public int getNumRows() { return 1; @@ -123,7 +129,8 @@ public class EmptyTextField implements Field { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { paintCursor(g, context.getCursorColor(), cursorLoc); } @@ -227,7 +234,7 @@ public class EmptyTextField implements Field { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - return new RowColLocation(0, 0); + return new DefaultRowColLocation(); } @Override 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 3715a0ac16..02fa87386d 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 @@ -30,155 +30,180 @@ import docking.widgets.fieldpanel.support.RowColLocation; public interface Field { /** - * Returns the current width of this field. + * Returns the current width of this field + * @return the current width of this field */ - int getWidth(); + public int getWidth(); /** * The minimum required width to paint the contents of this field * @return the minimum required width to paint the contents of this field */ - int getPreferredWidth(); + public int getPreferredWidth(); /** - * Returns the height of this field when populated with the given data. + * Returns the height of this field when populated with the given data + * @return the height */ - int getHeight(); + public int getHeight(); /** - * Returns the height above the baseLine. + * Returns the height above the baseLine + * @return the height above */ - int getHeightAbove(); + public int getHeightAbove(); /** - * Returns the height below the baseLine. + * Returns the height below the baseLine + * @return the height below */ - int getHeightBelow(); + public int getHeightBelow(); /** - * Returns the horizontal position of this field. + * Returns the horizontal position of this field + * @return the position */ - int getStartX(); + public int getStartX(); /** - * Paints this field. + * Paints this field * @param c the component to paint onto - * @param g the graphics context. + * @param g the graphics context * @param context common paint parameters - * @param clip the clipping region to paint into - * @param colorManager contains background color information for the field. + * @param clip the clipping region to paint into + * @param colorManager contains background color information for the field * @param cursorLoc the row,column cursor location within the field or null if the field does * not contain the cursor - * @param rowHeight the number of pixels in each row of text in the field. + * @param rowHeight the number of pixels in each row of text in the field */ - void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip, + public void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight); /** - * Returns true if the given point is in this field. - * @param x the horizontal coordinate of the point. - * @param y the relatve y position in this layout + * Returns true if the given point is in this field + * @param x the horizontal coordinate of the point + * @param y the relative y position in this layout + * @return true if the given point is in this field */ - boolean contains(int x, int y); + public boolean contains(int x, int y); + + /** + * Returns the number of data model rows represented by this field. Some fields may change + * the row count by wrapping or truncating. The value returned here will be the original data + * row count before any transformations were applied. + * @return the number of data rows + */ + public int getNumDataRows(); /** * Returns the number of rows in this field + * @return the number of rows in this field */ - int getNumRows(); + public int getNumRows(); /** - * Returns the number of columns in the given row. - * @param row the row from which to get the number of columns. + * Returns the number of columns in the given row + * @param row the row from which to get the number of columns; this is the screen row + * @return the number of columns */ - int getNumCols(int row); + public int getNumCols(int row); /** - * Returns the x coordinate for the given cursor position. - * @param row the text row of interest. - * @param col the character column. + * Returns the x coordinate for the given cursor position + * @param row the text row of interest + * @param col the character column + * @return the x value */ - int getX(int row, int col); + public int getX(int row, int col); /** - * Returns the y coordinate for the given row. - * @param row the text row of interest. + * Returns the y coordinate for the given row + * @param row the text row of interest + * @return the y value */ - int getY(int row); + public int getY(int row); /** - * Returns the row containing the given y coordinate. - * @param y vertical pixel coordinate relative to the top of the screen. + * Returns the row containing the given y coordinate + * @param y vertical pixel coordinate relative to the top of the screen + * @return the row */ - int getRow(int y); + public int getRow(int y); /** - * Returns the cursor column position for the given x coordinate on the given - * row. - * @param row the text row to find the column on. - * @param x the horizontal pixel coordinate for which to find the character position. + * Returns the cursor column position for the given x coordinate on the given row + * @param row the text row to find the column on + * @param x the horizontal pixel coordinate for which to find the character position + * @return the column */ - int getCol(int row, int x); + public int getCol(int row, int x); /** - * Returns true if the given row and column represent a valid location for - * this field with the given data; - * @param row the text row. - * @param col the character position. + * Returns true if the given row and column represent a valid location for this field with + * the given data + * @param row the text row + * @param col the character position + * @return tru if valid */ - boolean isValid(int row, int col); + public boolean isValid(int row, int col); /** - * Returns a bounding rectangle for the cursor at the given position. - * @param row the text row. - * @param col the character postion. + * Returns a bounding rectangle for the cursor at the given position + * @param row the text row + * @param col the character position + * @return the rectangle */ - Rectangle getCursorBounds(int row, int col); + public Rectangle getCursorBounds(int row, int col); /** * Returns the amount to scroll to the next or previous line - * @param topOfScreen - the current y pos of the top of the screen. - * @param direction - the direction of the scroll (1 down, -1 up) - * @param max - the maximum amount to scroll for the entire row - will - * be positive for down, and negative for up) + * @param topOfScreen the current y position of the top of the screen + * @param direction the direction of the scroll (1 down, -1 up) + * @param max the maximum amount to scroll for the entire row - will be positive for down, and + * negative for up) + * @return the scroll amount */ - int getScrollableUnitIncrement(int topOfScreen, int direction, int max); + public int getScrollableUnitIncrement(int topOfScreen, int direction, int max); /** - * Returns true if this field is "primary" (the most important) - * field; used to determine the "primary" line in the layout. + * Returns true if this field is "primary" (the most important) field; used to determine the + * "primary" line in the layout + * @return true if this field is "primary" */ - boolean isPrimary(); + public boolean isPrimary(); /** * notifies field that the rowHeight changed * @param heightAbove the height above the baseline - * @param heightBelow the height below the baseline. + * @param heightBelow the height below the baseline */ - void rowHeightChanged(int heightAbove, int heightBelow); + public void rowHeightChanged(int heightAbove, int heightBelow); /** - * Returns a string containing all the text in the field. + * Returns a string containing all the text in the field + * @return the string */ - String getText(); + public String getText(); /** - * Returns a string containing all the text in the field with extra linefeeds - * @return + * Returns a string containing all the text in the field with extra newlines + * @return a string containing all the text in the field with extra newlines */ - String getTextWithLineSeparators(); + public String getTextWithLineSeparators(); /** - * 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. + * 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. + * the overall field text */ - RowColLocation textOffsetToScreenLocation(int textOffset); + public RowColLocation textOffsetToScreenLocation(int textOffset); /** - * Returns the text offset in the overall field text string for the given row and column. - * @param row the row. - * @param col the column. + * Returns the text offset in the overall field text string for the given row and column + * @param row the row + * @param col the column + * @return the offset */ - int screenLocationToTextOffset(int row, int col); + public int screenLocationToTextOffset(int row, int col); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FieldElement.java index 70224eda7f..7b66d65eef 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FieldElement.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FieldElement.java @@ -23,7 +23,7 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.support.RowColLocation; /** - * Used by {@link Field}s to combine text, attributes and location information (for example to and + * Used by {@link Field}s to combine text, attributes and location information (for example to and * from screen and data locations). FieldFactory classes can use the various implementations * of this interface, or create new ones, to include additional information specific to the fields * that they create. @@ -37,14 +37,14 @@ public interface FieldElement { public String getText(); /** - * Returns the length of the text within this element. This is a convenience method for + * Returns the length of the text within this element. This is a convenience method for * calling getText().length(). * @return the length of the text within this element. */ public int length(); /** - * Returns the string width of this element. The width is based upon the associated + * Returns the string width of this element. The width is based upon the associated * FontMetrics object within this element. * @return the string width of this element. */ @@ -87,7 +87,7 @@ public interface FieldElement { public FieldElement substring(int start); /** - * Returns a new FieldElement containing just the characters beginning at the given start + * Returns a new FieldElement containing just the characters beginning at the given start * index (inclusive) and ending at the given end index (exclusive). * * @param start The starting index (inclusive) from which to substring this element. @@ -97,11 +97,11 @@ public interface FieldElement { public FieldElement substring(int start, int end); /** - * Returns a new FieldElement with all occurrences of the target characters replaced with the + * Returns a new FieldElement with all occurrences of the target characters replaced with the * given replacement character. * @param targets The array of characters to replace. * @param replacement The replacement character. - * @return a new FieldElement with all occurrences of the target characters replaced with the + * @return a new FieldElement with all occurrences of the target characters replaced with the * given replacement character. */ public FieldElement replaceAll(char[] targets, char replacement); @@ -111,13 +111,13 @@ public interface FieldElement { * element that will fit within the given width. * * @param width The width constraint - * @return the maximum number of characters from this field element that will fit within + * @return the maximum number of characters from this field element that will fit within * the given width. */ public int getMaxCharactersForWidth(int width); /** - * Translates the given character index to a data location related to the data model, as + * Translates the given character index to a data location related to the data model, as * determined by the FieldFactory. * * @param characterIndex The character index to translate. @@ -129,13 +129,15 @@ public interface FieldElement { * Returns the character index appropriate for the given data location * @param dataRow the row in the data model as determined by the creating field factory. * @param dataColumn the column in the data model as determined by the creating field factory. - * @return the character index appropriate for the given data location + * @return the character index appropriate for the given data location; -1 if this field does + * not contain the given location */ public int getCharacterIndexForDataLocation(int dataRow, int dataColumn); /** * Paints the text contained in this field element at the given x,y screen coordinate using the * given Graphics object. + * @param c the component being painted. * @param g the Graphics object used to paint the field text. * @param x the horizontal screen position to paint * @param y the vertical screen position to paint. @@ -144,7 +146,7 @@ public interface FieldElement { /** * Returns the inner-most FieldElement inside this field element at the given location - * @param column the charactor offset. + * @param column the character offset. * @return the inner-most FieldElement inside this field element at the given location */ public FieldElement getFieldElement(int column); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FlowLayoutTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FlowLayoutTextField.java index 762d9ea2c5..6b6d992fa9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FlowLayoutTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/FlowLayoutTextField.java @@ -15,67 +15,82 @@ */ package docking.widgets.fieldpanel.field; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import docking.widgets.fieldpanel.support.HighlightFactory; /** - * This class provides a TextField implementation that takes multiple - * AttributedStrings and places as many that will fit on a line without clipping - * before continuing to the next line. + * This class provides a TextField implementation that takes multiple AttributedStrings and places + * as many that will fit on a line without clipping before continuing to the next line. */ public class FlowLayoutTextField extends VerticalLayoutTextField { /** - * This constructor will create a text field that will render one line of - * text. If metrics.stringWidth(text) > width, then the text - * will be clipped. No wrapping will be performed. If text - * contains the highlight string, then it will be highlighted using the + * This constructor will create a text field that will render one line of text. If + * metrics.stringWidth(text) > width, then the text will be wrapped. + * If text contains the highlight string, then it will be highlighted using the * highlight color. * - * @param textElements - * the AttributedStrings to display - * @param startX - * the x position to draw the string - * @param width - * the max width allocated to this field - * @param maxLines - * the max number of lines to display - * @param hlFactory - * the highlight factory + * @param textElements the AttributedStrings to display + * @param startX the x position to draw the string + * @param width the max width allocated to this field + * @param maxLines the max number of lines to display + * @param hlFactory the highlight factory + * @deprecated use the constructor that takes a list */ + @Deprecated(since = "10.1", forRemoval = true) public FlowLayoutTextField(FieldElement[] textElements, int startX, int width, int maxLines, HighlightFactory hlFactory) { - super(createLineElements(textElements, width), startX, width, maxLines, hlFactory,""); + this(Arrays.asList(textElements), startX, width, maxLines, hlFactory); } - private static FieldElement[] createLineElements(FieldElement[] textElements, int width) { - List subFields = new ArrayList(); + /** + * This constructor will create a text field that will render one line of text. If + * metrics.stringWidth(text) > width, then the text will be wrapped. + * If text contains the highlight string, then it will be highlighted using the + * highlight color. + * + * @param elements the AttributedStrings to display + * @param startX the x position to draw the string + * @param width the max width allocated to this field + * @param maxLines the max number of lines to display + * @param hlFactory the highlight factory + */ + public FlowLayoutTextField(List elements, int startX, + int width, int maxLines, HighlightFactory hlFactory) { + super(createLineElements(elements, width), startX, width, maxLines, hlFactory, ""); + } + private static List createLineElements(List elements, + int width) { + List subFields = new ArrayList<>(); int currentIndex = 0; - while (currentIndex < textElements.length) { - int numberPerLine = getNumberOfElementsPerLine(textElements, currentIndex, width); - subFields.add(new CompositeFieldElement(textElements, currentIndex, numberPerLine)); + while (currentIndex < elements.size()) { + int numberPerLine = getNumberOfElementsPerLine(elements, currentIndex, width); + subFields.add(createLine(elements, currentIndex, numberPerLine)); currentIndex += numberPerLine; } - return subFields.toArray(new FieldElement[subFields.size()]); + return subFields; } - private static int getNumberOfElementsPerLine(FieldElement[] elements, int start, int width) { + private static CompositeFieldElement createLine(List elements, int from, + int length) { + return new CompositeFieldElement(elements.subList(from, from + length)); + } + + private static int getNumberOfElementsPerLine(List elements, int start, + int width) { int currentWidth = 0; int count = 0; - int n = elements.length; - for (int i = start; i < n; i++) { - currentWidth += elements[i].getStringWidth(); + for (FieldElement element : elements) { + currentWidth += element.getStringWidth(); count++; if (currentWidth > width) { return Math.max(count - 1, 1); } } - - return elements.length - start; + return elements.size() - start; } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ReverseClippingTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ReverseClippingTextField.java index 64d84cf8a7..7fe81e8e7b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ReverseClippingTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/ReverseClippingTextField.java @@ -97,7 +97,7 @@ public class ReverseClippingTextField implements TextField { } isClipped = true; - // get the index of the start character that will fit + // get the index of the start character that will fit startingCharIndex = textElement.getMaxCharactersForWidth(w - (availableWidth - DOT_DOT_DOT_WIDTH)) + 1; startingCharIndex = Math.min(startingCharIndex, textElement.length()); @@ -150,6 +150,11 @@ public class ReverseClippingTextField implements TextField { return textElement.length() + 1; // allow one column past the end of the text } + @Override + public int getNumDataRows() { + return 1; + } + @Override public int getNumRows() { return 1; @@ -230,7 +235,8 @@ public class ReverseClippingTextField implements TextField { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, + int rowHeight) { if (context.isPrinting()) { print(g, context); } @@ -341,7 +347,10 @@ public class ReverseClippingTextField implements TextField { @Override public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { int column = textElement.getCharacterIndexForDataLocation(dataRow, dataColumn); - return new RowColLocation(0, Math.max(column, 0)); + if (column < 0) { + return new DefaultRowColLocation(); + } + return new RowColLocation(0, column); } private int findX(int col) { @@ -389,7 +398,9 @@ public class ReverseClippingTextField implements TextField { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { int col = textOffset + startingCharIndex; - col = Math.max(col, 0); + if (col < 0) { + return new DefaultRowColLocation(); + } return new RowColLocation(0, Math.min(col, textElement.getText().length() - 1)); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleImageField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleImageField.java index f2f34a5aea..6db617874d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleImageField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleImageField.java @@ -22,6 +22,7 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; +import docking.widgets.fieldpanel.support.DefaultRowColLocation; import docking.widgets.fieldpanel.support.RowColLocation; /** @@ -46,7 +47,8 @@ public class SimpleImageField implements Field { * @param startY the starting y coordinate of the field. * @param width the width of the field. */ - public SimpleImageField(ImageIcon icon, FontMetrics metrics, int startX, int startY, int width) { + public SimpleImageField(ImageIcon icon, FontMetrics metrics, int startX, int startY, + int width) { this(icon, metrics, startX, startY, width, false); } @@ -115,6 +117,11 @@ public class SimpleImageField implements Field { return height; } + @Override + public int getNumDataRows() { + return 1; + } + @Override public int getNumRows() { return 1; @@ -180,7 +187,8 @@ public class SimpleImageField implements Field { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { if (icon == null) { return; } @@ -263,7 +271,7 @@ public class SimpleImageField implements Field { @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - return new RowColLocation(0, 0); + return new DefaultRowColLocation(); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleTextField.java index 7a3f3fb7fa..d86bbff75d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/SimpleTextField.java @@ -110,10 +110,15 @@ public class SimpleTextField implements Field { return startX; } + @Override + public int getNumDataRows() { + return 1; + } + /** - * - * @see docking.widgets.fieldpanel.field.Field#getNumRows() - */ + * + * @see docking.widgets.fieldpanel.field.Field#getNumRows() + */ @Override public int getNumRows() { return 1; @@ -199,7 +204,8 @@ public class SimpleTextField implements Field { @Override public void paint(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight) { + Rectangle clip, FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, + int rowHeight) { paintSelection(g, colorManager, 0); paintHighlights(g, hlFactory.getHighlights(this, text, -1)); g.setFont(metrics.getFont()); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyFieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/StrutFieldElement.java similarity index 72% rename from Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyFieldElement.java rename to Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/StrutFieldElement.java index 702ac62c15..c74069d6ae 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/EmptyFieldElement.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/StrutFieldElement.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,75 +23,94 @@ import javax.swing.JComponent; import docking.widgets.fieldpanel.support.RowColLocation; /** - * Used to force a clip to happen when the max lines is exceeded in the VerticalLayoutTextField + * Used to force a clip to happen by using this field with space characters and size that far + * exceeds the available painting width. */ - -public class EmptyFieldElement implements FieldElement { +public class StrutFieldElement implements FieldElement { private final int width; - public EmptyFieldElement(int width) { + public StrutFieldElement(int width) { this.width = width; - } + @Override public char charAt(int index) { return ' '; } + @Override public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) { - return 0; - } - - public Color getColor(int charIndex) { - return Color.BLACK; + return -1; // we have not characters } + @Override public RowColLocation getDataLocationForCharacterIndex(int characterIndex) { return new RowColLocation(0, 0); } - public FieldElement getFieldElement(int column) { + @Override + public Color getColor(int charIndex) { + return Color.BLACK; + } + + @Override + public FieldElement getFieldElement(int characterOffset) { return this; } + @Override public int getHeightAbove() { return 0; } + @Override public int getHeightBelow() { return 0; } + @Override public int getMaxCharactersForWidth(int stringWidth) { return 0; } + @Override public int getStringWidth() { return width; } + @Override public String getText() { return width == 0 ? "" : " "; } + @Override public int length() { return width == 0 ? 0 : 1; } + @Override public void paint(JComponent c, Graphics g, int x, int y) { + // nothing to paint } + @Override public FieldElement replaceAll(char[] targets, char replacement) { return this; } + @Override public FieldElement substring(int start) { - return new EmptyFieldElement(0); + return new StrutFieldElement(0); } + @Override public FieldElement substring(int start, int end) { - return new EmptyFieldElement(0); + return new StrutFieldElement(0); } + @Override + public String toString() { + return ""; // empty text placeholder + } } 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 b4735e9177..9a8789d7f1 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 @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,37 +15,40 @@ */ package docking.widgets.fieldpanel.field; +import docking.widgets.fieldpanel.support.DefaultRowColLocation; import docking.widgets.fieldpanel.support.RowColLocation; - public interface TextField extends Field { /** * Sets this field to be primary such that its row is primary + * @param b this field to be primary such that its row is primary */ public void setPrimary(boolean b); - - /** - * Translates a screen coordinate to a row and column in the data from the factory - * @param screenRow the row in the displayed field text. - * @param screenColumn the column in the displayed field text. - * @return a RowColLocation containing the row and column within the data from the factory. - */ - public RowColLocation screenToDataLocation(int screenRow, int screenColumn); - - /** - * Translates a data row and column into a screen row and column. - * @param dataRow row as defined by the factory - * @param dataColumn the character offset into the dataRow - * @return row and column in the screen coordinate system. - */ - public RowColLocation dataToScreenLocation(int dataRow, int dataColumn); - /** - * Returns true if the field is not displaying all the text information - */ + /** + * Translates a screen coordinate to a row and column in the data from the factory + * @param screenRow the row in the displayed field text. + * @param screenColumn the column in the displayed field text. + * @return a RowColLocation containing the row and column within the data from the factory. + */ + public RowColLocation screenToDataLocation(int screenRow, int screenColumn); + + /** + * Translates a data row and column into a screen row and column. + * @param dataRow row as defined by the factory + * @param dataColumn the character offset into the dataRow + * @return row and column in the screen coordinate system; a {@link DefaultRowColLocation} if + * this field does not contain the given column + */ + 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 @@ -54,5 +56,5 @@ public interface TextField extends Field { * @return the FieldElement at the given screen location. */ public FieldElement getFieldElement(int screenRow, int screenColumn); - + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextFieldElement.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextFieldElement.java index 6bbd3ac26f..beaed3377e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextFieldElement.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/TextFieldElement.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,27 +15,22 @@ */ package docking.widgets.fieldpanel.field; - -public final class TextFieldElement extends AbstractTextFieldElement { +public class TextFieldElement extends AbstractTextFieldElement { public TextFieldElement(AttributedString attributedString, int row, int column) { super(attributedString, row, column); } - /** - * @see docking.widgets.fieldpanel.field.FieldElement#substring(int, int) - */ + @Override public FieldElement substring(int start, int end) { AttributedString as = attributedString.substring(start, end); - if ( as == attributedString ) { + if (as == attributedString) { return this; } - return new TextFieldElement(as, row, column+start); + return new TextFieldElement(as, row, column + start); } - /** - * @see docking.widgets.fieldpanel.field.FieldElement#replaceAll(char[], char) - */ + @Override public FieldElement replaceAll(char[] targets, char replacement) { return new TextFieldElement(attributedString.replaceAll(targets, replacement), row, column); } 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 db4ff1b168..80b3b6ade0 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 @@ -16,25 +16,24 @@ package docking.widgets.fieldpanel.field; import java.awt.*; -import java.util.ArrayList; -import java.util.Collections; +import java.util.*; import java.util.List; import javax.swing.JComponent; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; import docking.widgets.fieldpanel.support.*; /** * This class provides a TextField implementation that takes multiple FieldElements and places - * each on its own line within the field. It also can take a single FieldElements and - * word wrap,creating new FieldElements (one per line). + * each on its own line within the field. */ public class VerticalLayoutTextField implements TextField { - protected FieldElement[] textElements; - protected List subFields; // list of fields for FieldElements + protected List subFields; // list of fields for FieldElements protected int startX; protected int width; protected int preferredWidth; @@ -42,11 +41,34 @@ public class VerticalLayoutTextField implements TextField { private int height; private int heightAbove; + private int numDataRows; private boolean isPrimary; - private String text; + + // full text is all text with line separators, *but not with line delimiters* + private String fullText; + private List lines; + + // used in the getText() method to separate rows without adding newlines + private String rowSeparator; protected boolean isClipped; - private String lineDelimiter; // used in the getText() method to separate lines + + /** + * This constructor will create a text field from an array of FieldElements, putting each + * element on its own line. + * + * @param textElements the FieldElements to display + * @param startX the x position to draw the element + * @param width the max width allocated to this field + * @param maxLines the max number of lines to display + * @param hlFactory the highlight factory + * @deprecated use the constructor that takes a list + */ + @Deprecated(since = "10.1", forRemoval = true) + public VerticalLayoutTextField(FieldElement[] textElements, int startX, int width, int maxLines, + HighlightFactory hlFactory) { + this(Arrays.asList(textElements), startX, width, maxLines, hlFactory, " "); + } /** * This constructor will create a text field from an array of FieldElements, putting each @@ -58,7 +80,8 @@ public class VerticalLayoutTextField implements TextField { * @param maxLines the max number of lines to display * @param hlFactory the highlight factory */ - public VerticalLayoutTextField(FieldElement[] textElements, int startX, int width, int maxLines, + public VerticalLayoutTextField(List textElements, int startX, int width, + int maxLines, HighlightFactory hlFactory) { this(textElements, startX, width, maxLines, hlFactory, " "); } @@ -72,25 +95,46 @@ public class VerticalLayoutTextField implements TextField { * @param width the max width allocated to this field * @param maxLines the max number of lines to display * @param hlFactory the highlight factory - * @param lineDelimiter The string to space lines of text when concatenated by the + * @param rowSeparator The string used to space lines of text when concatenated by the * getText() method. */ - protected VerticalLayoutTextField(FieldElement[] textElements, int startX, int width, - int maxLines, HighlightFactory hlFactory, String lineDelimiter) { + protected VerticalLayoutTextField(List textElements, int startX, int width, + int maxLines, HighlightFactory hlFactory, String rowSeparator) { - this.textElements = textElements; this.startX = startX; this.width = width; - this.hlFactory = hlFactory; - this.lineDelimiter = lineDelimiter; + this.rowSeparator = rowSeparator; - subFields = layoutElements(maxLines); + lines = generateLines(textElements); + fullText = generateText(textElements, rowSeparator); + subFields = layoutElements(textElements, maxLines); + numDataRows = textElements.size(); - this.preferredWidth = calculatePreferredWidth(); + preferredWidth = calculatePreferredWidth(); calculateHeight(); } + private List generateLines(List textElements) { + + List list = new ArrayList<>(); + for (FieldElement field : textElements) { + list.add(field.getText()); + } + return list; + } + + private String generateText(List elements, String delimiter) { + + StringBuilder buf = new StringBuilder(); + int n = elements.size() - 1; + for (int i = 0; i < n; i++) { + buf.append(elements.get(i).getText()).append(delimiter); + } + buf.append(elements.get(n).getText()); + return buf.toString(); + } + protected void calculateHeight() { heightAbove = (subFields.get(0)).getHeightAbove(); for (Field field : subFields) { @@ -108,15 +152,12 @@ public class VerticalLayoutTextField implements TextField { @Override public String getText() { - if (text == null) { - text = generateText(); - } - return text; + return fullText; } @Override public String getTextWithLineSeparators() { - return generateText("\n"); + return StringUtils.join(lines, '\n'); } @Override @@ -144,6 +185,11 @@ public class VerticalLayoutTextField implements TextField { return startX; } + @Override + public int getNumDataRows() { + return numDataRows; + } + @Override public int getNumRows() { return subFields.size(); @@ -157,11 +203,11 @@ public class VerticalLayoutTextField implements TextField { @Override public int getRow(int y) { - if (y < -heightAbove) { + if (y < 0) { return 0; } - int heightSoFar = -heightAbove; + int heightSoFar = 0; int n = subFields.size(); for (int i = 0; i < n; i++) { Field f = subFields.get(i); @@ -247,7 +293,7 @@ public class VerticalLayoutTextField implements TextField { int startY = myStartY; int translatedY = 0; - + int extraSpace = rowSeparator.length(); for (int i = 0; i < n; i++) { ClippingTextField subField = (ClippingTextField) subFields.get(i); int subFieldHeight = subField.getHeight(); @@ -276,7 +322,7 @@ public class VerticalLayoutTextField implements TextField { startY += subFieldHeight; g.translate(0, subFieldHeight); translatedY += subFieldHeight; - columns += subField.getText().length() + lineDelimiter.length(); + columns += subField.getText().length() + extraSpace; } // restore the graphics to where it was when we started. @@ -355,6 +401,7 @@ public class VerticalLayoutTextField implements TextField { /** * Returns the list of subfields in this field. + * @return the list of subfields in this field. */ public List getSubfields() { return Collections.unmodifiableList(subFields); @@ -372,27 +419,29 @@ public class VerticalLayoutTextField implements TextField { @Override public void rowHeightChanged(int heightAbove1, int heightBelow) { - // most fields don't care + // most fields don't care } @Override public FieldElement getFieldElement(int screenRow, int screenColumn) { - FieldElement clickedField = textElements[screenRow]; - return clickedField.getFieldElement(screenColumn); + TextField f = subFields.get(screenRow); + + int fieldRow = 0; // each field is on a single row + return f.getFieldElement(fieldRow, screenColumn); } - protected List layoutElements(int maxLines) { - List newSubFields = new ArrayList<>(); + protected List layoutElements(List textElements, int maxLines) { + List newSubFields = new ArrayList<>(); - boolean tooManyLines = textElements.length > maxLines; + boolean tooManyLines = textElements.size() > maxLines; - for (int i = 0; i < textElements.length && i < maxLines; i++) { - FieldElement element = textElements[i]; + for (int i = 0; i < textElements.size() && i < maxLines; i++) { + FieldElement element = textElements.get(i); if (tooManyLines && (i == maxLines - 1)) { FieldElement[] elements = new FieldElement[2]; elements[0] = element; - elements[1] = new EmptyFieldElement(500); + elements[1] = new StrutFieldElement(500); element = new CompositeFieldElement(elements); } TextField field = new ClippingTextField(startX, width, element, hlFactory); @@ -406,83 +455,70 @@ public class VerticalLayoutTextField implements TextField { } /** - * Translates the row and column to a String index and character offset into + * Translates the row and column to a String index and character offset into * that string. * @param screenRow the row containing the location. * @param screenColumn the character position in the row of the location - * @return a MultiStringLocation containing the string index and position + * @return a MultiStringLocation containing the string index and position * within that string. */ @Override public RowColLocation screenToDataLocation(int screenRow, int screenColumn) { - screenRow = Math.min(screenRow, textElements.length - 1); + screenRow = Math.min(screenRow, subFields.size() - 1); screenRow = Math.max(screenRow, 0); - screenColumn = Math.min(screenColumn, textElements[screenRow].length()); + TextField field = subFields.get(screenRow); + screenColumn = Math.min(screenColumn, field.getText().length()); screenColumn = Math.max(screenColumn, 0); - return textElements[screenRow].getDataLocationForCharacterIndex(screenColumn); + int fieldRow = 0; // each field is on a single row + return field.screenToDataLocation(fieldRow, screenColumn); } - /** - * Finds the corresponding row, column for string index, and offset - * @param dataRow index into the string array - * @param dataColumn offset into the indexed string. - */ @Override public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { - for (int screenRow = textElements.length - 1; screenRow >= 0; screenRow--) { - FieldElement element = textElements[screenRow]; - int screenColumn = element.getCharacterIndexForDataLocation(dataRow, dataColumn); - if (screenColumn >= 0) { - return new RowColLocation(screenRow, screenColumn); - } + + if (dataRow >= getNumRows()) { + TextField lastField = subFields.get(subFields.size()); + return new DefaultRowColLocation(lastField.getText().length(), subFields.size() - 1); } - return new RowColLocation(0, 0); // give up - } - - protected String generateText() { - return generateText(lineDelimiter); - } - - protected String generateText(String delimiter) { - StringBuffer buf = new StringBuffer(); - int n = textElements.length - 1; - for (int i = 0; i < n; i++) { - buf.append(textElements[i].getText()).append(delimiter); - } - buf.append(textElements[n].getText()); - return buf.toString(); + TextField field = subFields.get(dataRow); + RowColLocation location = field.dataToScreenLocation(dataRow, dataColumn); + return location.withRow(dataRow); } @Override public int screenLocationToTextOffset(int row, int col) { - if (row >= textElements.length) { + if (row >= subFields.size()) { return getText().length(); } - int extraSpace = lineDelimiter.length(); + int extraSpace = rowSeparator.length(); int len = 0; for (int i = 0; i < row; i++) { - len += textElements[i].getText().length() + extraSpace; + len += lines.get(i).length() + extraSpace; } - len += Math.min(col, textElements[row].getText().length()); + len += Math.min(col, lines.get(row).length()); return len; } @Override public RowColLocation textOffsetToScreenLocation(int textOffset) { - int extraSpace = lineDelimiter.length(); - int n = textElements.length; + int absoluteOffset = textOffset; + int extraSpace = rowSeparator.length(); + int n = subFields.size(); for (int i = 0; i < n; i++) { - int len = textElements[i].getText().length(); - if (textOffset < len + extraSpace) { - return new RowColLocation(i, textOffset); + int len = lines.get(i).length(); + if (absoluteOffset < len + extraSpace) { + return new RowColLocation(i, absoluteOffset); } - textOffset -= len + extraSpace; + absoluteOffset -= len + extraSpace; } - return new RowColLocation(n - 1, textElements[n - 1].getText().length()); + + int lastRow = n - 1; + int lastColumn = subFields.get(lastRow).getText().length(); + return new DefaultRowColLocation(lastRow, lastColumn); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappingVerticalLayoutTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappingVerticalLayoutTextField.java index 2244301df5..2fe0578fab 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappingVerticalLayoutTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/field/WrappingVerticalLayoutTextField.java @@ -17,13 +17,16 @@ package docking.widgets.fieldpanel.field; import docking.widgets.fieldpanel.support.*; +/** + * A text field meant to take a string of text and wrap as needed. + */ public class WrappingVerticalLayoutTextField extends VerticalLayoutTextField { /** - * This constructor will create a text field from an single AttributedString. The string will + * This constructor will create a text field from an single AttributedString. The string will * be word wrapped. * - * @param textElement the AttributedString to display + * @param textElement the element to display * @param startX the x position to draw the string * @param width the max width allocated to this field * @param maxLines the max number of lines to display @@ -31,12 +34,13 @@ public class WrappingVerticalLayoutTextField extends VerticalLayoutTextField { */ public WrappingVerticalLayoutTextField(FieldElement textElement, int startX, int width, int maxLines, HighlightFactory hlFactory) { - - super(FieldUtils.wrap(textElement, width), startX, width, maxLines, hlFactory, ""); + super(FieldUtils.wrap(textElement, width), startX, width, maxLines, hlFactory, " "); } /** - * Create a text field from a single FieldElement. The text is wrapped, either an words or simply + * This constructor will create a text field from an single AttributedString. The string will + * be word wrapped. + * * @param textElement is the element to display * @param startX is the position to draw the string * @param width is the max width allocated to this field @@ -47,16 +51,12 @@ public class WrappingVerticalLayoutTextField extends VerticalLayoutTextField { public WrappingVerticalLayoutTextField(FieldElement textElement, int startX, int width, int maxLines, HighlightFactory hlFactory, boolean breakOnWhiteSpace) { super(FieldUtils.wrap(textElement, width, breakOnWhiteSpace), startX, width, maxLines, - hlFactory, ""); + hlFactory, " "); } - /** - * Finds the corresponding row, column for string index, and offset - * @param index index into the string array - * @param offset offset into the indexed string. - */ @Override - public RowColLocation dataToScreenLocation(int index, int offset) { - return textOffsetToScreenLocation(offset); + public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { + // we represent one data row that may be split into multiple screen rows + return textOffsetToScreenLocation(dataColumn); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/DefaultRowColLocation.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/DefaultRowColLocation.java new file mode 100644 index 0000000000..bd3ec096a2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/DefaultRowColLocation.java @@ -0,0 +1,41 @@ +/* ### + * 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.support; + +/** + * A location used to represent a an edge case where not suitable location can be found and the + * client does not wish to return null. + */ +public class DefaultRowColLocation extends RowColLocation { + public DefaultRowColLocation() { + super(0, 0); + } + + public DefaultRowColLocation(int row, int col) { + super(row, col); + } + + @Override + public RowColLocation withCol(int newColumn) { + return new DefaultRowColLocation(row, newColumn); + } + + @Override + public RowColLocation withRow(int newRow) { + return new DefaultRowColLocation(newRow, col); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldSelectionHelper.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldSelectionHelper.java index e07974df5a..022891dbd2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldSelectionHelper.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldSelectionHelper.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,9 +34,12 @@ public class FieldSelectionHelper { } - /** + /** * Gets the selected text that pertains to an individual field. Null is returned if the * given selection spans more than one field. + * @param selection the selection + * @param panel the field panel + * @return the text */ public static String getFieldSelectionText(FieldSelection selection, FieldPanel panel) { if (!isStringSelection(selection)) { @@ -46,9 +48,14 @@ public class FieldSelectionHelper { return getTextForField(selection.getFieldRange(0), panel); } - /** Returns the text within the given selection. */ + /** + * Returns the text within the given selection. + * @param selection the selection + * @param panel the field panel + * @return the text + */ public static String getAllSelectedText(FieldSelection selection, FieldPanel panel) { - StringBuffer buffy = new StringBuffer(); + StringBuilder buffy = new StringBuilder(); int numRanges = selection.getNumRanges(); for (int i = 0; i < numRanges; i++) { FieldRange fieldRange = selection.getFieldRange(i); @@ -99,7 +106,7 @@ public class FieldSelectionHelper { int startFieldNumber = startLoc.fieldNum; BigInteger endIndex = endLoc.getIndex(); - StringBuffer buffy = new StringBuffer(); + StringBuilder buffy = new StringBuilder(); for (BigInteger i = startIndex; i.compareTo(endIndex) <= 0; i = i.add(BigInteger.ONE)) { Layout layout = panel.getLayoutModel().getLayout(i); String text = null; @@ -133,7 +140,7 @@ public class FieldSelectionHelper { private static String getTextForFieldsInLayout(Layout layout, FieldRange fieldRange, int startFieldNumber, int endFieldNumber) { - StringBuffer buffy = new StringBuffer(); + StringBuilder buffy = new StringBuilder(); for (int i = startFieldNumber; i < endFieldNumber; i++) { Field field = layout.getField(i); buffy.append(field.getTextWithLineSeparators()); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldUtils.java index 5e0ddbab88..8a3130dc7b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/FieldUtils.java @@ -15,8 +15,7 @@ */ package docking.widgets.fieldpanel.support; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import docking.widgets.fieldpanel.field.FieldElement; @@ -42,14 +41,14 @@ public class FieldUtils { * Splits the given FieldElement into sub-elements by wrapping the element on whitespace. * * @param fieldElement The element to wrap - * @param width The maximum width to allow before wrapping + * @param width The maximum width to allow before wrapping * @return The wrapped elements */ - public static FieldElement[] wrap(FieldElement fieldElement, int width) { + public static List wrap(FieldElement fieldElement, int width) { FieldElement originalFieldElement = fieldElement.replaceAll(WHITE_SPACE, ' '); if (originalFieldElement.getStringWidth() <= width) { - return new FieldElement[] { originalFieldElement }; + return Arrays.asList(originalFieldElement); } List lines = new ArrayList<>(); @@ -63,7 +62,7 @@ public class FieldUtils { wordWrapPos = findWordWrapPosition(originalFieldElement, width); } lines.add(originalFieldElement); - return lines.toArray(new FieldElement[lines.size()]); + return lines; } /** @@ -75,14 +74,14 @@ public class FieldUtils { * @param breakOnWhiteSpace determines whether line breaks should happen at white space chars * @return the wrapped elements */ - public static FieldElement[] wrap(FieldElement fieldElement, int width, + public static List wrap(FieldElement fieldElement, int width, boolean breakOnWhiteSpace) { if (breakOnWhiteSpace) { return wrap(fieldElement, width); } FieldElement originalFieldElement = fieldElement.replaceAll(WHITE_SPACE, ' '); if (originalFieldElement.getStringWidth() <= width) { - return new FieldElement[] { originalFieldElement }; + return Arrays.asList(originalFieldElement); } List lines = new ArrayList<>(); @@ -99,14 +98,14 @@ public class FieldUtils { } } lines.add(originalFieldElement); - return lines.toArray(new FieldElement[lines.size()]); + return lines; } /** * Splits the given FieldElement into sub-elements by wrapping the element on whitespace. * * @param fieldElement The element to wrap - * @param width The maximum width to allow before wrapping + * @param width The maximum width to allow before wrapping * @return The wrapped elements */ public static List wordWrapList(FieldElement fieldElement, int width) { @@ -133,10 +132,11 @@ public class FieldUtils { /** * Finds the position within the given element at which to split the line for word wrapping. - * This method only breaks on whitespace characters. It finds the last whitespace character - * that completely fits within the given width. If there is no whitespace character before - * the width break point, it finds the first whitespace character after the width. If the - * element cannot be split at all, it returns 0. + * This method finds the last whitespace character that completely fits within the given width. + * If there is no whitespace character before the width break point, it finds the first + * whitespace character after the width. If no whitespace can be found, then the text will + * be split at a non-whitespace character. + * * @param element the element to split * @param width the max width to allow before looking for a word wrap positions * @return 0 if the element cannot be split, else the character position of the string @@ -156,30 +156,19 @@ public class FieldUtils { } return wrapPosition; - // The following code was replace with the return just above. This has the effect - // of splitting contiguous words at the field width instead of at the next white - // space beyond. -// whiteSpacePosition = text.indexOf(" ", wrapPosition); -// if (whiteSpacePosition >= 0) { -// if (whiteSpacePosition + 1 >= element.length()) { // if whitespace at end, no split -// return 0; -// } -// return whiteSpacePosition; -// } -// return 0; } /** - * Trims "goofy" characters off of the given label, like spaces, '[',']', etc. + * Trims unwanted characters off of the given label, like spaces, '[',']', etc. * @param string The string to be trimmed * @return The trimmed string. */ public static String trimString(String string) { // short-circuit case where the given string starts normally, but contains invalid // characters (e.g., param_1[EAX]) - StringBuffer buffer = new StringBuffer(string); + StringBuilder buffer = new StringBuilder(string); if (Character.isJavaIdentifierPart(buffer.charAt(0))) { - // in this case just take all valid characters and then exit + // in this case just take all valid characters and then exit for (int index = 1; index < buffer.length(); index++) { int charAt = buffer.charAt(index); if (!Character.isJavaIdentifierPart(charAt)) { @@ -189,7 +178,7 @@ public class FieldUtils { return buffer.toString(); } - // the following case is when the given string is surrounded by "goofy" characters + // the following case is when the given string is surrounded by "goofy" characters int index = 0; int charAt = buffer.charAt(index); while (!Character.isJavaIdentifierPart(charAt) && buffer.length() > 0) { 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 4cb0def1d8..6821b56458 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 @@ -19,8 +19,9 @@ package docking.widgets.fieldpanel.support; * Simple class to return a row, column location. */ public class RowColLocation { - private int row; - private int col; + protected int row; + protected int col; + /** * Constructs a new RowColLocation with the given row and column. * @param row the row location @@ -30,36 +31,48 @@ public class RowColLocation { this.row = row; this.col = col; } - /** - * Returns the row. - */ + public int row() { return row; } - /** - * Returns the column. - */ + public int col() { return col; } - /** - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "RowColLocation("+row+","+col+")"; - } - + + public RowColLocation withCol(int newColumn) { + return new RowColLocation(row, newColumn); + } + + public RowColLocation withRow(int newRow) { + return new RowColLocation(newRow, col); + } + @Override - public boolean equals( Object object ) { + public String toString() { + return row + "," + col; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + col; + result = prime * result + row; + return result; + } + + @Override + public boolean equals(Object object) { if (object == null) { return false; } - if ( object.getClass() == RowColLocation.class ) { - RowColLocation loc = (RowColLocation) object; - return (row == loc.row) && (col == loc.col); + + if (!getClass().equals(object.getClass())) { + return false; } - return false; + + RowColLocation loc = (RowColLocation) object; + return (row == loc.row) && (col == loc.col); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java index f0890fcc08..0b8bf1916e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java @@ -72,7 +72,7 @@ public class RowLayout implements Layout { // Can only compress the last field, as the rest are potentially part of a grid surrounded // by other layouts // - // Notes: we have to account for any offset for fields that are disabled and are in + // Notes: we have to account for any offset for fields that are disabled and are in // the beginning of the row. // int startX = fields[0].getStartX(); @@ -180,8 +180,9 @@ public class RowLayout implements Layout { gapIndex = fields.length; } int startX = - gapIndex == 0 ? rect.x : fields[gapIndex - 1].getStartX() + - fields[gapIndex - 1].getWidth(); + gapIndex == 0 ? rect.x + : fields[gapIndex - 1].getStartX() + + fields[gapIndex - 1].getWidth(); int endX = gapIndex >= fields.length ? rect.x + rect.width : fields[gapIndex].getStartX(); if (startX < endX) { @@ -201,7 +202,7 @@ public class RowLayout implements Layout { Field field = fields[index]; cursorLoc.fieldNum = index; - cursorLoc.row = field.getRow(y - heightAbove); + cursorLoc.row = field.getRow(y); cursorLoc.col = field.getCol(cursorLoc.row, x); return field.getX(cursorLoc.row, cursorLoc.col); } 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 604bd46201..31682df7e7 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 @@ -18,6 +18,8 @@ package docking.widgets.fieldpanel; import static org.junit.Assert.*; import java.awt.*; +import java.util.ArrayList; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -32,10 +34,6 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest { private FlowLayoutTextField textField; - public FlowLayoutTextFieldTest() { - super(); - } - @SuppressWarnings("deprecation") // we mean to use getFontMetrics @Before public void setUp() throws Exception { @@ -47,156 +45,18 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest { Font font = new Font("Times New Roman", 0, 14); Toolkit tk = Toolkit.getDefaultToolkit(); FontMetrics fm = tk.getFontMetrics(font); - FieldElement[] elements = new FieldElement[4]; - elements[0] = new TextFieldElement(new AttributedString("Hello ", Color.BLUE, fm), 0, 0); - elements[1] = new TextFieldElement( - new AttributedString("World ", Color.RED, fm, true, Color.BLUE), 1, 0); - elements[2] = - new TextFieldElement(new AttributedString(CLIPPED_STRING, Color.GREEN, fm), 2, 0); - elements[3] = new TextFieldElement(new AttributedString("Wow! ", Color.GRAY, fm), 3, 0); + List elements = new ArrayList<>(); + + elements.add(new TextFieldElement(new AttributedString("Hello ", Color.BLUE, fm), 0, 0)); + elements.add(new TextFieldElement( + new AttributedString("World ", Color.RED, fm, true, Color.BLUE), 1, 0)); + elements.add( + new TextFieldElement(new AttributedString(CLIPPED_STRING, Color.GREEN, fm), 2, 0)); + elements.add(new TextFieldElement(new AttributedString("Wow! ", Color.GRAY, fm), 3, 0)); textField = new FlowLayoutTextField(elements, 100, 100, 3, factory); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getWidth()' - */ - @Test - public void testGetWidth() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getHeight()' - */ - @Test - public void testGetHeight() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getStartX()' - */ - @Test - public void testGetStartX() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getNumRows()' - */ - @Test - public void testGetNumRows() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getNumCols(int)' - */ - @Test - public void testGetNumCols() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getRow(int)' - */ - @Test - public void testGetRow() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getCol(int, int)' - */ - @Test - public void testGetCol() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getY(int)' - */ - @Test - public void testGetY() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getX(int, int)' - */ - @Test - public void testGetX() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.isValid(int, int)' - */ - @Test - public void testIsValid() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.paint(Graphics, PaintContext, boolean)' - */ - @Test - public void testPaint() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.paintCursor(Graphics, PaintContext, boolean, int, int)' - */ - @Test - public void testPaintCursor() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getCursorBounds(int, int)' - */ - @Test - public void testGetCursorBounds() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.contains(int, int)' - */ - @Test - public void testContains() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getScrollableUnitIncrement(int, int, int)' - */ - @Test - public void testGetScrollableUnitIncrement() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.isPrimary()' - */ - @Test - public void testIsPrimary() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.setPrimary(boolean)' - */ - @Test - public void testSetPrimary() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getStringLocation(int, int)' - */ @Test public void testScreenToDataLocation() { assertEquals(new RowColLocation(0, 0), textField.screenToDataLocation(0, 0)); @@ -222,9 +82,6 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest { assertEquals(new RowColLocation(3, 5), textField.screenToDataLocation(50, 75)); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getWrappedLocation(int, int)' - */ @Test public void testDataToScreenLocation() { assertEquals(new RowColLocation(0, 0), textField.dataToScreenLocation(0, 0)); @@ -247,9 +104,6 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest { assertEquals(new RowColLocation(0, 0), textField.dataToScreenLocation(0, 75)); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getRowColumn(int)' - */ @Test public void testGetRowColumn() { assertEquals(new RowColLocation(0, 0), textField.textOffsetToScreenLocation(0)); @@ -264,55 +118,5 @@ public class FlowLayoutTextFieldTest extends AbstractGenericTest { assertEquals(new RowColLocation(1, 18), textField.textOffsetToScreenLocation(30)); assertEquals(new RowColLocation(2, 5), textField.textOffsetToScreenLocation(1000)); - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getSubfields()' - */ - @Test - public void testGetSubfields() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getHeightAbove()' - */ - @Test - public void testGetHeightAbove() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getHeightBelow()' - */ - @Test - public void testGetHeightBelow() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.rowHeightChanged(int, int)' - */ - @Test - public void testRowHeightChanged() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getText()' - */ - @Test - public void testGetText() { - - } - - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getTextOffset(int, int)' - */ - @Test - public void testGetTextOffset() { - - } - } 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 dacf10ab15..4a88f74358 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 @@ -18,6 +18,8 @@ package docking.widgets.fieldpanel; import static org.junit.Assert.*; import java.awt.*; +import java.util.ArrayList; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -32,10 +34,6 @@ public class VerticalLayoutTextFieldTest extends AbstractGenericTest { private VerticalLayoutTextField textField; - public VerticalLayoutTextFieldTest() { - super(); - } - @SuppressWarnings("deprecation") // we mean to use getFontMetrics @Before public void setUp() throws Exception { @@ -47,20 +45,19 @@ public class VerticalLayoutTextFieldTest extends AbstractGenericTest { Font font = new Font("Times New Roman", 0, 14); Toolkit tk = Toolkit.getDefaultToolkit(); FontMetrics fm = tk.getFontMetrics(font); - FieldElement[] elements = new FieldElement[4]; - elements[0] = new TextFieldElement(new AttributedString("Hello", Color.BLUE, fm), 0, 0); - elements[1] = new TextFieldElement( - new AttributedString("World", Color.RED, fm, true, Color.BLUE), 1, 0); - elements[2] = - new TextFieldElement(new AttributedString(CLIPPED_STRING, Color.GREEN, fm), 2, 0); - elements[3] = new TextFieldElement(new AttributedString("Wow!", Color.GRAY, fm), 3, 0); + + List elements = new ArrayList<>(); + + elements.add(new TextFieldElement(new AttributedString("Hello", Color.BLUE, fm), 0, 0)); + elements.add(new TextFieldElement( + new AttributedString("World", Color.RED, fm, true, Color.BLUE), 1, 0)); + elements.add( + new TextFieldElement(new AttributedString(CLIPPED_STRING, Color.GREEN, fm), 2, 0)); + elements.add(new TextFieldElement(new AttributedString("Wow!", Color.GRAY, fm), 3, 0)); textField = new VerticalLayoutTextField(elements, 100, 100, 5, factory); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getStringLocation(int, int)' - */ @Test public void testScreenToDataLocation() { assertEquals(new RowColLocation(0, 0), textField.screenToDataLocation(0, 0)); @@ -81,11 +78,8 @@ public class VerticalLayoutTextFieldTest extends AbstractGenericTest { assertEquals(new RowColLocation(3, 4), textField.screenToDataLocation(50, 75)); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getWrappedLocation(int, int)' - */ @Test - public void testGetWrappedLocation() { + public void testDataToScreenLocation() { assertEquals(new RowColLocation(0, 0), textField.dataToScreenLocation(0, 0)); assertEquals(new RowColLocation(0, 2), textField.dataToScreenLocation(0, 2)); assertEquals(new RowColLocation(0, 5), textField.dataToScreenLocation(0, 5)); @@ -96,26 +90,26 @@ public class VerticalLayoutTextFieldTest extends AbstractGenericTest { assertEquals(new RowColLocation(2, 0), textField.dataToScreenLocation(2, 0)); assertEquals(new RowColLocation(2, 4), textField.dataToScreenLocation(2, 4)); - assertEquals(new RowColLocation(2, 15), textField.dataToScreenLocation(2, 15)); + assertEquals(new RowColLocation(2, 12), textField.dataToScreenLocation(2, 12)); + assertEquals(new DefaultRowColLocation(2, 12), textField.dataToScreenLocation(2, 15)); assertEquals(new RowColLocation(3, 0), textField.dataToScreenLocation(3, 0)); assertEquals(new RowColLocation(3, 4), textField.dataToScreenLocation(3, 4)); } - /* - * Test method for 'ghidra.util.bean.field.WrappingTextField.getRowColumn(int)' - */ @Test - public void testGetRowColumn() { + public void testTextOffsetToScreenLocation() { assertEquals(new RowColLocation(0, 0), textField.textOffsetToScreenLocation(0)); assertEquals(new RowColLocation(0, 5), textField.textOffsetToScreenLocation(5)); + assertEquals(new RowColLocation(1, 0), textField.textOffsetToScreenLocation(6)); assertEquals(new RowColLocation(1, 4), textField.textOffsetToScreenLocation(10)); assertEquals(new RowColLocation(1, 5), textField.textOffsetToScreenLocation(11)); + assertEquals(new RowColLocation(2, 0), textField.textOffsetToScreenLocation(12)); assertEquals(new RowColLocation(1, 4), textField.textOffsetToScreenLocation(10)); - assertEquals(new RowColLocation(3, 4), textField.textOffsetToScreenLocation(1000)); + assertEquals(new DefaultRowColLocation(3, 4), textField.textOffsetToScreenLocation(1000)); } } 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 new file mode 100644 index 0000000000..4f9f8c51b8 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/field/CompositeVerticalLayoutTextFieldTest.java @@ -0,0 +1,586 @@ +/* ### + * 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 static org.junit.Assert.*; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import docking.widgets.fieldpanel.support.*; +import generic.test.AbstractGenericTest; + +public class CompositeVerticalLayoutTextFieldTest extends AbstractGenericTest { + + private static final String LONG_STRING = "Supercalifragilisticexpialidocious"; + private FontMetrics fontMetrics; + + private CompositeVerticalLayoutTextField field; + private List rows; + + // some default field values + int startX = 100; // arbitrary + int width = 100; // used to trigger horizontal clipping + int maxLines = 5; // used to trigger vertical clipping + + private HighlightFactory hlFactory = (hlField, text, cursorTextOffset) -> { + return new Highlight[] {}; + }; + + @Before + public void setUp() throws Exception { + + Font font = new Font("Times New Roman", 0, 14); + fontMetrics = getFontMetrics(font); + + field = createField(maxLines, List.of( + "Hello", + "World", + LONG_STRING, + "Wow!")); + } + + private CompositeVerticalLayoutTextField createField(int lineLimit, List lines) { + + rows = lines; + + List elements = new ArrayList<>(); + int row = 0; + for (String line : lines) { + elements.add(createRow(row++, line, Color.BLUE)); + } + + List fields = new ArrayList<>(); + for (FieldElement element : elements) { + fields.add(new ClippingTextField(startX, width, element, hlFactory)); + } + + return new CompositeVerticalLayoutTextField(fields, startX, width, lineLimit, hlFactory); + } + + private CompositeVerticalLayoutTextField createBasicWrappingField(List lines) { + + rows = lines; + + List elements = new ArrayList<>(); + int row = 0; + for (String line : lines) { + elements.add(createRow(row++, line, Color.BLUE)); + } + + List fields = new ArrayList<>(); + for (FieldElement element : elements) { + fields.add(new WrappingVerticalLayoutTextField(element, startX, width, maxLines, + hlFactory)); + } + + return new CompositeVerticalLayoutTextField(fields, startX, width, maxLines, hlFactory); + } + + private CompositeVerticalLayoutTextField createMixedWrappingField(List lines) { + + rows = lines; + + int row = 0; + List fields = new ArrayList<>(); + fields.add(wrappedField(row++, lines.get(0))); + fields.add(clippedField(row++, lines.get(1))); + fields.add(wrappedField(row++, lines.get(2))); + + return new CompositeVerticalLayoutTextField(fields, startX, width, maxLines, hlFactory); + } + + private CompositeVerticalLayoutTextField createMixedWrappingField(TextField... fields) { + return new CompositeVerticalLayoutTextField(Arrays.asList(fields), startX, width, maxLines, + hlFactory); + } + + private TextField wrappedField(int row, String text) { + FieldElement element = createRow(row, text, Color.BLUE); + return new WrappingVerticalLayoutTextField(element, startX, width, maxLines, hlFactory); + } + + private TextField clippedField(int row, String text) { + FieldElement element = createRow(row, text, Color.BLUE); + return new ClippingTextField(startX, width, element, hlFactory); + } + + private FieldElement createRow(int row, String text, Color color) { + return new TextFieldElement(new AttributedString(text, color, fontMetrics), row, 0); + } + + @Test + public void testScreenToDataLocation() { + + assertRowCol(0, 0, field.screenToDataLocation(0, 0)); + assertRowCol(0, 2, field.screenToDataLocation(0, 2)); + assertRowCol(0, 5, field.screenToDataLocation(0, 5)); + assertRowCol(0, 5, field.screenToDataLocation(0, 6)); // past end + assertRowCol(0, 5, field.screenToDataLocation(0, 75)); + + assertRowCol(1, 0, field.screenToDataLocation(1, 0)); + assertRowCol(1, 5, field.screenToDataLocation(1, 6)); + assertRowCol(1, 5, field.screenToDataLocation(1, 16)); + + assertRowCol(2, 0, field.screenToDataLocation(2, 0)); + assertRowCol(2, 4, field.screenToDataLocation(2, 4)); + assertRowCol(2, 34, field.screenToDataLocation(2, 75)); + + assertRowCol(3, 0, field.screenToDataLocation(3, 0)); + assertRowCol(3, 4, field.screenToDataLocation(50, 75)); + } + + @Test + public void testDataToScreenLocation() { + assertRowCol(0, 0, field.dataToScreenLocation(0, 0)); + assertRowCol(0, 2, field.dataToScreenLocation(0, 2)); + assertRowCol(0, 5, field.dataToScreenLocation(0, 5)); + + assertRowCol(1, 0, field.dataToScreenLocation(1, 0)); + assertRowCol(1, 4, field.dataToScreenLocation(1, 4)); + assertRowCol(1, 5, field.dataToScreenLocation(1, 5)); + + 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)); + + assertRowCol(3, 0, field.dataToScreenLocation(3, 0)); + assertRowCol(3, 4, field.dataToScreenLocation(3, 4)); + } + + @Test + public void testTextOffsetToScreenLocation() { + + // + // Each row of text has text.lenghth() + 1 possible positions: before and after each + // character. For example, in the text "hi", these are the possible cursor positions: + // + // |hi + // h|i + // hi| + // + + // each line may have a line separator + int separator = field.getRowSeparator().length(); + + // dumpFieldOffsets(); + + // the end is after the last character + String row1 = rows.get(0); + int row1End = row1.length(); + assertRowCol(0, 0, field.textOffsetToScreenLocation(0)); + assertRowCol(0, row1End - 1, field.textOffsetToScreenLocation(row1End - 1)); + assertRowCol(0, row1End, field.textOffsetToScreenLocation(row1End)); + + int row2Start = row1End + separator; + String row2 = rows.get(1); + int row2End = row2Start + row2.length(); + int relativeEnd = row2End - row2Start; + assertRowCol(1, 0, field.textOffsetToScreenLocation(row2Start)); + assertRowCol(1, relativeEnd - 1, field.textOffsetToScreenLocation(row2End - 1)); + assertRowCol(1, relativeEnd, field.textOffsetToScreenLocation(row2End)); + + String row3 = rows.get(2); + int row3Start = row2End + separator; + assertRowCol(2, 0, field.textOffsetToScreenLocation(row3Start)); + + int row3End = row3Start + row3.length(); + int row4Start = row3End + 1; + assertRowCol(3, 0, field.textOffsetToScreenLocation(row4Start)); + + // far past the end will put the cursor at the end + String row4 = rows.get(3); + assertRowCol(3, row4.length(), field.textOffsetToScreenLocation(1000)); + } + + @Test + public void testScreenLocationToTextOffset() { + + // each line may have a line separator + int separator = field.getRowSeparator().length(); + + String row1 = rows.get(0); + int row1End = row1.length(); + assertEquals(0, field.screenLocationToTextOffset(0, 0)); + assertEquals(row1End - 1, field.screenLocationToTextOffset(0, row1End - 1)); + assertEquals(row1End, field.screenLocationToTextOffset(0, row1End)); + + int row2Start = row1End + separator; + String row2 = rows.get(1); + int row2End = row2Start + row2.length(); + int relativeEnd = row2End - row2Start; + assertEquals(row2Start, field.screenLocationToTextOffset(1, 0)); + assertEquals(row2End - 1, field.screenLocationToTextOffset(1, relativeEnd - 1)); + assertEquals(row2End, field.screenLocationToTextOffset(1, relativeEnd)); + + String row3 = rows.get(2); + int row3Start = row2End + separator; + assertEquals(row3Start, field.screenLocationToTextOffset(2, 0)); + + int row3End = row3Start + row3.length(); + int row4Start = row3End + 1; + assertEquals(row4Start, field.screenLocationToTextOffset(3, 0)); + assertRowCol(3, 0, field.textOffsetToScreenLocation(row4Start)); + + // far past the end will put the cursor at the end + String row4 = rows.get(3); + int row4End = row4Start + row4.length(); + assertEquals(row4End, field.screenLocationToTextOffset(3, 1000)); + } + + @Test + public void testGetFieldElement() { + + String row1 = rows.get(0); + assertEquals(row1, field.getFieldElement(0, 0).toString()); + assertEquals(row1, field.getFieldElement(0, 1).toString()); + assertEquals(row1, field.getFieldElement(0, row1.length()).toString()); + assertEquals(row1, field.getFieldElement(0, row1.length() + 1).toString()); + assertEquals(row1, field.getFieldElement(0, 100).toString()); + + String row2 = rows.get(1); + assertEquals(row2, field.getFieldElement(1, 0).toString()); + assertEquals(row2, field.getFieldElement(1, 1).toString()); + assertEquals(row2, field.getFieldElement(1, row2.length()).toString()); + assertEquals(row2, field.getFieldElement(1, row2.length() + 1).toString()); + assertEquals(row2, field.getFieldElement(1, 100).toString()); + + String row3 = rows.get(2); + assertEquals(row3, field.getFieldElement(2, 0).toString()); + assertEquals(row3, field.getFieldElement(2, 1).toString()); + assertEquals(row3, field.getFieldElement(2, row3.length()).toString()); + assertEquals(row3, field.getFieldElement(2, row3.length() + 1).toString()); + assertEquals(row3, field.getFieldElement(2, 100).toString()); + + String row4 = rows.get(3); + assertEquals(row4, field.getFieldElement(3, 0).toString()); + assertEquals(row4, field.getFieldElement(3, 1).toString()); + assertEquals(row4, field.getFieldElement(3, row4.length()).toString()); + assertEquals(row4, field.getFieldElement(3, row4.length() + 1).toString()); + assertEquals(row4, field.getFieldElement(3, 100).toString()); + } + + @Test + public void testGetNumColumns() { + + int separator = field.getRowSeparator().length(); + + String row1 = rows.get(0); + int row1Columns = row1.length() + separator; + assertEquals(row1Columns, field.getNumCols(0)); + + String row2 = rows.get(1); + int row2Columns = row2.length() + separator; + assertEquals(row2Columns, field.getNumCols(1)); + + // note: the number of columns is the clipped text length, which is 12, plus 1 extra + // column to allow for placing the cursor after the text + int clippedLength = 13; // not sure how to get this from the field + assertEquals(clippedLength, field.getNumCols(2)); + + String row4 = rows.get(3); + int row4Columns = row4.length() + separator; + assertEquals(row4Columns, field.getNumCols(3)); + } + + @Test + public void testClippingWithTooManyRows() { + + int lineLimit = 2; + field = createField(lineLimit, List.of( + "Hello", + "Wolrd", + LONG_STRING, + "Wow!")); + + assertEquals(2, field.getNumRows()); + assertEquals(4, field.getNumDataRows()); + + assertRowCol(0, 0, field.dataToScreenLocation(0, 0)); + assertRowCol(0, 2, field.dataToScreenLocation(0, 2)); + assertRowCol(0, 5, field.dataToScreenLocation(0, 5)); + + assertRowCol(1, 0, field.dataToScreenLocation(1, 0)); + assertRowCol(1, 4, field.dataToScreenLocation(1, 4)); + assertRowCol(1, 5, field.dataToScreenLocation(1, 5)); + + // try accessing clipped rows + assertRowCol(1, 0, field.dataToScreenLocation(2, 0)); + assertRowCol(1, 5, field.dataToScreenLocation(2, 5)); + assertRowCol(1, 5, field.dataToScreenLocation(20, 50)); + } + + @Test + public void testIsClipped_NoClipping() { + field = createField(maxLines, List.of("Hello", "Wolrd")); + assertFalse(field.isClipped()); + } + + @Test + public void testIsClipped_HorizontalClipping() { + field = createField(maxLines, List.of(LONG_STRING)); + assertTrue(field.isClipped()); + } + + @Test + public void testIsClipped_VerticalClipping() { + int lineLimit = 2; + field = createField(lineLimit, List.of( + "Hello", + "Wolrd", + "Wow!")); + assertTrue(field.isClipped()); + } + + @Test + public void testIsClipped_HorizontalAndVerticalClipping() { + int lineLimit = 2; + field = createField(lineLimit, List.of( + "Hello", + "Wolrd", + LONG_STRING, + "Wow!")); + assertTrue(field.isClipped()); + } + + @Test + public void getGetAllRows() { + + String row1 = rows.get(0); + List allRows = field.getAllRowsUpTo(0); + assertEquals(1, allRows.size()); + assertEquals(row1, allRows.get(0).toString()); + + String row2 = rows.get(1); + allRows = field.getAllRowsUpTo(1); + assertEquals(2, allRows.size()); + assertEquals(row1, allRows.get(0).toString()); + assertEquals(row2, allRows.get(1).toString()); + + String row3 = rows.get(2); + allRows = field.getAllRowsUpTo(2); + assertEquals(3, allRows.size()); + assertEquals(row1, allRows.get(0).toString()); + assertEquals(row2, allRows.get(1).toString()); + assertEquals(row3, allRows.get(2).toString()); + + String row4 = rows.get(3); + allRows = field.getAllRowsUpTo(3); + assertEquals(4, allRows.size()); + assertEquals(row1, allRows.get(0).toString()); + assertEquals(row2, allRows.get(1).toString()); + assertEquals(row3, allRows.get(2).toString()); + assertEquals(row4, allRows.get(3).toString()); + + allRows = field.getAllRowsUpTo(10); + assertEquals(4, allRows.size()); + + allRows = field.getAllRowsUpTo(-1); + assertEquals(0, allRows.size()); + } + + @Test + public void testGetText() { + assertEquals("Hello World Supercalifragilisticexpialidocious Wow! ", field.getText()); + } + + @Test + public void testGetTextWithLineSeparators() { + assertEquals("Hello\nWorld\nSupercalifragilisticexpialidocious\nWow!", + field.getTextWithLineSeparators()); + } + + @Test + public void testGetY_And_GetRow() { + + int y = field.getY(0); + int row = field.getRow(y); + assertEquals("Wrong row for y value: " + y, 0, row); + + y = field.getY(1); + row = field.getRow(y); + assertEquals("Wrong row for y value: " + y, 1, row); + + y = field.getY(2); + row = field.getRow(y); + assertEquals("Wrong row for y value: " + y, 2, row); + + y = field.getY(3); + row = field.getRow(y); + assertEquals("Wrong row for y value: " + y, 3, row); + + // try values past the end + int yForRowTooBig = field.getY(10); + assertEquals(y, yForRowTooBig); + int rowForYTooBig = field.getRow(1000); + assertEquals(3, rowForYTooBig); + + // try values before the beginning + int yForRowTooSmall = field.getY(-1); + int expectedY = -field.getHeightAbove(); + assertEquals(expectedY, yForRowTooSmall); + int rowForYTooSmall = field.getRow(-1000); + assertEquals(0, rowForYTooSmall); + } + + @Test + public void testGetX_And_GetCol() { + + String row1 = rows.get(0); + int x = field.getX(0, 0); + int column = field.getCol(0, x); + assertEquals(0, column); + + x = field.getX(0, row1.length()); + column = field.getCol(0, x); + assertEquals(row1.length(), column); + + String row2 = rows.get(1); + x = field.getX(1, 0); + column = field.getCol(1, x); + assertEquals(0, column); + + x = field.getX(1, row2.length()); + column = field.getCol(1, x); + assertEquals(row2.length(), column); + + String row3 = rows.get(2); + x = field.getX(2, 0); + column = field.getCol(2, x); + assertEquals(0, column); + + x = field.getX(2, row3.length()); + column = field.getCol(2, x); + int clippedLength = 12; // not sure how to get this from the field + assertEquals(clippedLength, column); + + x = field.getX(2, row3.length() + 1); + column = field.getCol(2, x); + assertEquals(clippedLength, column); + } + + @Test + public void testLayoutWithWrapping_OneWrappedRow() { + + // + // Test the composite field when one of the internal fields will wrap into multiple rows + // when too long. + // + + field = + createBasicWrappingField(List.of("This is a line with multiple words for wrapping")); + + assertEquals(4, field.getNumRows()); + + assertEquals("This is a line", field.getFieldElement(0, 0).getText()); + assertEquals("with multiple", field.getFieldElement(1, 0).getText()); + assertEquals("words for", field.getFieldElement(2, 0).getText()); + assertEquals("wrapping", field.getFieldElement(3, 0).getText()); + + // note: the final 'data' row becomes 4 'screen' rows + assertEquals(15, field.getNumCols(0)); + assertEquals(14, field.getNumCols(1)); + assertEquals(10, field.getNumCols(2)); + assertEquals(9, field.getNumCols(3)); + } + + @Test + public void testLayoutWrapping_TwoWrappedRow() { + + field = createBasicWrappingField(List.of("This is line one", "This is line two")); + + assertEquals(4, field.getNumRows()); + + assertEquals("This is line", field.getFieldElement(0, 0).getText()); + assertEquals("one", field.getFieldElement(1, 0).getText()); + assertEquals("This is line", field.getFieldElement(2, 0).getText()); + assertEquals("two", field.getFieldElement(3, 0).getText()); + + // note: the final 'data' row becomes 4 'screen' rows + assertEquals(13, field.getNumCols(0)); + assertEquals(4, field.getNumCols(1)); + assertEquals(13, field.getNumCols(2)); + assertEquals(4, field.getNumCols(3)); + } + + @Test + public void testLayoutWrapping_MixedRows() { + + // + // Test that we can mix wrapping and non-wrapping rows + // + + field = createMixedWrappingField( + List.of("This is line one", "This line does not wrap", "This is line two")); + + assertEquals(5, field.getNumRows()); + + assertEquals("This is line", field.getFieldElement(0, 0).getText()); + assertEquals("one", field.getFieldElement(1, 0).getText()); + assertEquals("This line does not wrap", field.getFieldElement(2, 0).getText()); + assertEquals("This is line", field.getFieldElement(3, 0).getText()); + assertEquals("two", field.getFieldElement(4, 0).getText()); + + // note: the final 'data' row becomes 5 'screen' rows + assertEquals(13, field.getNumCols(0)); + assertEquals(4, field.getNumCols(1)); + assertEquals(14, field.getNumCols(2)); + assertEquals(13, field.getNumCols(3)); + assertEquals(4, field.getNumCols(4)); + } + + @Test + public void testLayoutWrapping_MixedRows_TrailingWrappingRow() { + + String row1 = "1: clipped row: This will be clipped horizonally"; + String row2 = "2: clipped row: This will be clipped horizonally"; + String row3 = "3: wrapped row: This field will wrap"; + TextField field1 = clippedField(0, row1); + TextField field2 = clippedField(1, row2); + TextField field3 = wrappedField(2, row3); + + field = createMixedWrappingField(field1, field2, field3); + + assertEquals(5, field.getNumRows()); + + assertEquals(row1, field.getFieldElement(0, 0).getText()); + assertEquals(row2, field.getFieldElement(1, 0).getText()); + assertEquals("3: wrapped", field.getFieldElement(2, 0).getText()); + assertEquals("row: This field", field.getFieldElement(3, 0).getText()); + assertEquals("will wrap", field.getFieldElement(4, 0).getText()); + + // not sure how to get this from the field + int clippedLength = 14; + assertEquals(clippedLength, field.getNumCols(0)); + assertEquals(clippedLength, field.getNumCols(1)); + + // note: the final 'data' row becomes 3 'screen' rows + assertEquals(11, field.getNumCols(2)); + assertEquals(16, field.getNumCols(3)); + assertEquals(10, field.getNumCols(4)); + } + + private void assertRowCol(int expectedRow, int expectedColumn, RowColLocation actualLocation) { + assertEquals("Wrong row", expectedRow, actualLocation.row()); + assertEquals("Wrong column", expectedColumn, actualLocation.col()); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java index e069eabb60..de7beb67e8 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.awt.*; import java.awt.event.*; +import java.awt.image.BufferedImage; import java.io.*; import java.lang.reflect.*; import java.net.*; @@ -219,7 +220,7 @@ public abstract class AbstractGenericTest extends AbstractGTest { /** * A callback for subclasses when a test has failed. This will be called - * after tearDown(). This means that any diagnostics will have to + * after tearDown(). This means that any diagnostics will have to * take into account items that have already been disposed. * * @param e the exception that happened when the test failed @@ -1118,7 +1119,7 @@ public abstract class AbstractGenericTest extends AbstractGTest { } /** - * Call this version of {@link #runSwing(Runnable)} when you expect your runnable may + * Call this version of {@link #runSwing(Runnable)} when you expect your runnable may * throw exceptions * * @param callback the runnable code snippet to call @@ -1148,13 +1149,13 @@ public abstract class AbstractGenericTest extends AbstractGTest { public static void runSwing(Runnable runnable, boolean wait) { - // + // // Special Case: this check handled re-entrant test code. That is, an calls to runSwing() // that are made from within a runSwing() call. Most clients do not do // this, but it can happen when a client makes a test API call (which itself - // calls runSwing()) from within a runSwing() call. - // - // Calling the run method directly here ensures that the order of client + // calls runSwing()) from within a runSwing() call. + // + // Calling the run method directly here ensures that the order of client // requests is preserved. // if (SwingUtilities.isEventDispatchThread()) { @@ -1167,7 +1168,7 @@ public abstract class AbstractGenericTest extends AbstractGTest { return; } - // don't wait; invoke later; catch any exceptions ourselves in order to fail-fast + // don't wait; invoke later; catch any exceptions ourselves in order to fail-fast Runnable swingExceptionCatcher = () -> { try { runnable.run(); @@ -1243,11 +1244,11 @@ public abstract class AbstractGenericTest extends AbstractGTest { doRun(swingExceptionCatcher); } catch (InterruptedException | InvocationTargetException e) { - // Assume that if we have an exception reported by our catcher, then that is + // Assume that if we have an exception reported by our catcher, then that is // the root cause of this exception and do not report this one. The typical // exception here is an InterrruptedException that is caused by our test - // harness when it is interrupting the test thread after a previous Swing - // exception that we have detected--we don't care to report the + // harness when it is interrupting the test thread after a previous Swing + // exception that we have detected--we don't care to report the // InterruptedException, as we caused it. The InvocationTargetException should // be handled by our runnable above. } @@ -1561,6 +1562,19 @@ public abstract class AbstractGenericTest extends AbstractGTest { UIManager.put("TextArea.font", f); } + /** + * Returns a font metrics for the given font using a generic buffered image graphics context. + * @param font the font + * @return the font metrics + */ + public static FontMetrics getFontMetrics(Font font) { + BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics g = image.getGraphics(); + FontMetrics fm = g.getFontMetrics(font); + g.dispose(); + return fm; + } + /** * Signals that the client expected the System Under Test (SUT) to report errors. Use this * when you wish to verify that errors are reported and you do not want those errors to @@ -1582,7 +1596,7 @@ public abstract class AbstractGenericTest extends AbstractGTest { //================================================================================================== // Swing Methods -//================================================================================================== +//================================================================================================== /** * Waits for the Swing thread to process any pending events. This method @@ -1685,11 +1699,11 @@ public abstract class AbstractGenericTest extends AbstractGTest { // calls all execute in the Swing thread in a blocking fashion, so when we are done // flushing, there should be no more work scheduled due to us flushing. Due to other // potential background threads though, more work may be scheduled as we are working. - // Thus, for fast tests, you should not have background work happening that is not + // Thus, for fast tests, you should not have background work happening that is not // directly related to your code being tested. // - // arbitrary; we have at least one level of a manager triggering another manager, + // arbitrary; we have at least one level of a manager triggering another manager, // which would be 2 int n = 3; for (int i = 0; i < n; i++) { @@ -1743,8 +1757,8 @@ public abstract class AbstractGenericTest extends AbstractGTest { SwingUtilities.invokeAndWait(empty); } catch (Exception e) { - // Assumption: since our runnable is empty, this can only an interrupted - // exception, which can happen if our test framework decides to + // Assumption: since our runnable is empty, this can only an interrupted + // exception, which can happen if our test framework decides to // shut the operation down. return; }