diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerMarginService.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerMarginService.java new file mode 100644 index 0000000000..4dc074aded --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerMarginService.java @@ -0,0 +1,37 @@ +/* ### + * 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.decompiler; + +import ghidra.app.decompiler.component.margin.DecompilerMarginProvider; + +/** + * A service that allows clients to add custom margins in the Decompiler UI. + */ +public interface DecompilerMarginService { + /** + * Add a margin to the Decompiler's primary window + * + * @param provider the margin provider + */ + void addMarginProvider(DecompilerMarginProvider provider); + + /** + * Remove a margin from the Decompiler's primary window + * + * @param provider the margin provider + */ + void removeMarginProvider(DecompilerMarginProvider provider); +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/CDisplayPanel.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/CDisplayPanel.java index 940158a67a..dd4a231bb3 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/CDisplayPanel.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/CDisplayPanel.java @@ -159,7 +159,7 @@ public class CDisplayPanel extends JPanel implements DecompilerCallbackHandler { } @Override - public void doWheNotBusy(Callback c) { + public void doWhenNotBusy(Callback c) { // stub } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangLayoutController.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangLayoutController.java index 861c9bb6e1..84a5084516 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangLayoutController.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangLayoutController.java @@ -21,8 +21,6 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.*; -import javax.swing.JComponent; - import org.apache.commons.lang3.StringUtils; import docking.widgets.SearchLocation; @@ -47,10 +45,7 @@ import ghidra.util.Msg; */ public class ClangLayoutController implements LayoutModel, LayoutModelListener { - private final ClangFieldElement EMPTY_LINE_NUMBER_SPACER; - private int maxWidth; - private int lineNumberFieldWidth; private int indentWidth; private DecompileOptions options; private DecompilerPanel decompilerPanel; @@ -59,26 +54,19 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { private FontMetrics metrics; private HighlightFactory hlFactory; private ArrayList listeners; - private Color[] syntax_color; // Foreground colors. + private Color[] syntaxColor; // Foreground colors. private BigInteger numIndexes = BigInteger.ZERO; private ArrayList lines = new ArrayList<>(); private boolean showLineNumbers = true; - private ClangFieldElement createEmptyLineNumberSpacer() { - ClangToken lineNumberToken = ClangToken.buildSpacer(null, 0, ""); - AttributedString as = new AttributedString("", Color.WHITE, metrics); - return new ClangFieldElement(lineNumberToken, as, 0); - } - public ClangLayoutController(DecompileOptions opt, DecompilerPanel decompilerPanel, FontMetrics met, HighlightFactory hlFactory) { options = opt; this.decompilerPanel = decompilerPanel; - syntax_color = new Color[ClangToken.MAX_COLOR]; + syntaxColor = new Color[ClangToken.MAX_COLOR]; metrics = met; this.hlFactory = hlFactory; - EMPTY_LINE_NUMBER_SPACER = createEmptyLineNumberSpacer(); listeners = new ArrayList<>(); buildLayouts(null, null, null, false); } @@ -94,7 +82,7 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { @Override public Dimension getPreferredViewSize() { - return new Dimension(maxWidth + lineNumberFieldWidth, 500); + return new Dimension(maxWidth, 500); } @Override @@ -179,15 +167,11 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { boolean paintLineNumbers) { List tokens = line.getAllTokens(); - ClangFieldElement lineNumberFieldElement = - createLineNumberFieldElement(line, lineCount, paintLineNumbers); - FieldElement[] elements = createFieldElementsForLine(tokens); int indent = line.getIndent() * indentWidth; - int lineNumberWidth = lineNumberFieldElement.getStringWidth(); - int updatedMaxWidth = maxWidth + lineNumberWidth; - return new ClangTextField(tokens, elements, lineNumberFieldElement, indent, updatedMaxWidth, + int updatedMaxWidth = maxWidth; + return new ClangTextField(tokens, elements, indent, line.getLineNumber(), updatedMaxWidth, hlFactory); } @@ -197,7 +181,7 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { int columnPosition = 0; for (int i = 0; i < tokens.size(); ++i) { ClangToken token = tokens.get(i); - Color color = syntax_color[token.getSyntaxType()]; + Color color = syntaxColor[token.getSyntaxType()]; if (token instanceof ClangCommentToken) { AttributedString prototype = new AttributedString("prototype", color, metrics); Program program = decompilerPanel.getProgram(); @@ -214,32 +198,22 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { return elements; } - private ClangFieldElement createLineNumberFieldElement(ClangLine line, int lineCount, - boolean paintLineNumbers) { - - if (paintLineNumbers) { - return new LineNumberFieldElement(line.getLineNumber(), lineCount, metrics); - } - - return EMPTY_LINE_NUMBER_SPACER; - } - /** * Update to the current Decompiler display options */ @SuppressWarnings("deprecation") // ignoring the deprecated call for toolkit private void updateOptions() { - syntax_color[ClangToken.KEYWORD_COLOR] = options.getKeywordColor(); - syntax_color[ClangToken.TYPE_COLOR] = options.getTypeColor(); - syntax_color[ClangToken.FUNCTION_COLOR] = options.getFunctionColor(); - syntax_color[ClangToken.COMMENT_COLOR] = options.getCommentColor(); - syntax_color[ClangToken.VARIABLE_COLOR] = options.getVariableColor(); - syntax_color[ClangToken.CONST_COLOR] = options.getConstantColor(); - syntax_color[ClangToken.PARAMETER_COLOR] = options.getParameterColor(); - syntax_color[ClangToken.GLOBAL_COLOR] = options.getGlobalColor(); - syntax_color[ClangToken.DEFAULT_COLOR] = options.getDefaultColor(); - syntax_color[ClangToken.ERROR_COLOR] = options.getErrorColor(); + syntaxColor[ClangToken.KEYWORD_COLOR] = options.getKeywordColor(); + syntaxColor[ClangToken.TYPE_COLOR] = options.getTypeColor(); + syntaxColor[ClangToken.FUNCTION_COLOR] = options.getFunctionColor(); + syntaxColor[ClangToken.COMMENT_COLOR] = options.getCommentColor(); + syntaxColor[ClangToken.VARIABLE_COLOR] = options.getVariableColor(); + syntaxColor[ClangToken.CONST_COLOR] = options.getConstantColor(); + syntaxColor[ClangToken.PARAMETER_COLOR] = options.getParameterColor(); + syntaxColor[ClangToken.GLOBAL_COLOR] = options.getGlobalColor(); + syntaxColor[ClangToken.DEFAULT_COLOR] = options.getDefaultColor(); + syntaxColor[ClangToken.ERROR_COLOR] = options.getErrorColor(); // setting the metrics here will indirectly trigger the new font to be used deeper in // the bowels of the FieldPanel (you can get the font from the metrics) @@ -247,7 +221,6 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { metrics = Toolkit.getDefaultToolkit().getFontMetrics(font); indentWidth = metrics.stringWidth(PrettyPrinter.INDENT_STRING); maxWidth = indentWidth * options.getMaxWidth(); - lineNumberFieldWidth = 0; showLineNumbers = options.isDisplayLineNumbers(); } @@ -264,11 +237,6 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { fieldList = new Field[lineCount]; // One field for each "C" line numIndexes = BigInteger.valueOf(lineCount); - lineNumberFieldWidth = 0; - if (showLineNumbers && !isError) { - lineNumberFieldWidth = LineNumberFieldElement.getFieldWidth(metrics, lineCount); - } - for (int i = 0; i < lineCount; ++i) { ClangLine oneLine = lines.get(i); fieldList[i] = createTextFieldForLine(oneLine, lineCount, showLineNumbers); @@ -600,81 +568,6 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener { // Inner Classes //================================================================================================== - private static class LineNumberFieldElement extends ClangFieldElement { - private static final Color FOREGROUND_COLOR = new Color(125, 125, 125); - private int uniformWidth; - - private LineNumberFieldElement(int lineNumber, int lineCount, FontMetrics fontMetrics) { - super(ClangToken.buildSpacer(null, 0, ""), createAttributedLineNumberString(lineNumber, - lineCount, FOREGROUND_COLOR, fontMetrics), 0); - uniformWidth = calculateUniformStringWidth(fontMetrics); - } - - private static String createLineNumberString(int lineNumber, int lineCount) { - - String lineCountString = Integer.toString(lineCount); - int maxNumberOfDigits = lineCountString.length(); - - String lineNumberString = Integer.toString(lineNumber); - int lineNumberLength = lineNumberString.length(); - int padLength = maxNumberOfDigits - lineNumberLength; - - StringBuffer buffy = new StringBuffer(); - for (int i = 0; i < padLength; i++) { - buffy.append(' '); - } - buffy.append(lineNumberString).append(' '); // space for separation - return buffy.toString(); - } - - private static AttributedString createAttributedLineNumberString(int lineNumber, - int lineCount, Color foregroundColor, FontMetrics fontMetrics) { - return new AttributedString(createLineNumberString(lineNumber, lineCount), - foregroundColor, fontMetrics); - } - - static int getFieldWidth(FontMetrics fontMetrics, int lineCnt) { - int largestCharacterWidth = getLargestCharacterWidth(fontMetrics); - int numberOfCharacters = createLineNumberString(0, lineCnt).length(); - return numberOfCharacters * largestCharacterWidth; - } - - private int calculateUniformStringWidth(FontMetrics fontMetrics) { - int largestCharacterWidth = getLargestCharacterWidth(fontMetrics); - int numberOfCharacters = getText().length(); - return numberOfCharacters * largestCharacterWidth; - } - - private static int getLargestCharacterWidth(FontMetrics fontMetrics) { - // use the biggest number char (since that's what we paint in this object) - // for determining the a space to use as a guide - return fontMetrics.stringWidth("9"); - } - - @Override - public void paint(JComponent c, Graphics g, int x, int y) { - // paint our text - super.paint(c, g, 0, 0); - - // paint a vertical rule - Color color = getColor(0); - g.setColor(color); - - FontMetrics fontMetrics = g.getFontMetrics(); - int topX = fontMetrics.getMaxAscent() + 1; // fudge for font painting differences - int maxDescent = fontMetrics.getMaxDescent(); - - int baselineX = maxDescent + 1; // fudge for font painting differences - g.drawLine(uniformWidth, -topX, uniformWidth, baselineX); - } - - @Override - // overridden so that our width reflects our custom width - public int getStringWidth() { - return uniformWidth + 3; // fudge for keeping the c code off the line number bar - } - } - private class FieldNumberColumnPair { private final int fieldNumber; private final int column; 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 bacb407a59..535f20d397 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 @@ -15,68 +15,32 @@ */ package ghidra.app.decompiler.component; -import java.awt.Graphics; -import java.awt.Rectangle; import java.util.List; -import javax.swing.JComponent; - import docking.widgets.fieldpanel.field.*; -import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; -import docking.widgets.fieldpanel.internal.PaintContext; -import docking.widgets.fieldpanel.support.*; +import docking.widgets.fieldpanel.support.FieldLocation; +import docking.widgets.fieldpanel.support.HighlightFactory; import ghidra.app.decompiler.ClangToken; public class ClangTextField extends WrappingVerticalLayoutTextField { private List tokenList; - private FieldElement lineNumberFieldElement; + private final int lineNumber; private static FieldElement createSingleLineElement(FieldElement[] textElements) { return new CompositeFieldElement(textElements); } - /** - * Calculates the offset of the x position for the given line number element. The line - * numbers of the decompiler appear to the left of the data and thus offset the actual data - * by the width of the line numbers. The line numbers may be disabled, in which case the - * given FieldElement will have no width. - * - * @param initialX The original x value passed to the constructor of this class - * @param lineNumberElement he line number element for this field from which we get a width - * @return the calculated offset - */ - private static int calculateXPositionWithLineNumberOffset(int initialX, - FieldElement lineNumberElement) { - return initialX + lineNumberElement.getStringWidth(); - } - - /** - * Calculates the modified width for this field. This is a factor of line numbers and any - * x offset given to this field element. - * - * @param initialX The original x value passed to the constructor of this class - * @param lineNumberElement The line number element for this field from which we get a width - * @param initialWidth The initial width we are allowed to take up - * @return the modified width for this field. This is a factor of line numbers and any - * x offset given to this field element. - */ - private static int calculateWidthFromXPosition(int initialX, FieldElement lineNumberElement, - int initialWidth) { - return initialWidth - calculateXPositionWithLineNumberOffset(initialX, lineNumberElement); - } - - public ClangTextField(List tokenList, FieldElement[] fieldElements, - FieldElement lineNumberFieldElement, int x, int width, HighlightFactory hlFactory) { - super(createSingleLineElement(fieldElements), - calculateXPositionWithLineNumberOffset(x, lineNumberFieldElement), - calculateWidthFromXPosition(x, lineNumberFieldElement, width), 30, hlFactory, false); + public ClangTextField(List tokenList, FieldElement[] fieldElements, int x, + int lineNumber, int width, HighlightFactory hlFactory) { + super(createSingleLineElement(fieldElements), x, width - x, 30, hlFactory, false); this.tokenList = tokenList; - this.lineNumberFieldElement = lineNumberFieldElement; + this.lineNumber = lineNumber; } /** * Gets the C language token at the indicated location. + * * @param loc the field location * @return the token */ @@ -96,11 +60,11 @@ public class ClangTextField extends WrappingVerticalLayoutTextField { } /** - * Returns the token that is completely after the token that contains the given column - * location. In this case, 'contains' means any position inside of a token, but - * not at the beginning. So, if the column location is in the middle of a - * token, it will return the index of next token. But if the column location is at - * the beginning (just before the start) of a token, it will return the index of that token. + * Returns the token that is completely after the token that contains the given column location. + * In this case, 'contains' means any position inside of a token, but not at the + * beginning. So, if the column location is in the middle of a token, it will return the index + * of next token. But if the column location is at the beginning (just before the start) of a + * token, it will return the index of that token. * * @param location containing the column at which to beginning searching * @return the next token starting after the given column @@ -156,51 +120,7 @@ public class ClangTextField extends WrappingVerticalLayoutTextField { return tokenList; } - @Override - public void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip, - FieldBackgroundColorManager selectionMap, RowColLocation cursorLoc, int rowHeight) { - - // Don't print line numbers; don't copy line numbers. We are assuming that the user only - // wants to copy code. - if (context.isPrinting() || context.isTextCopying()) { - printTextWithoutLineNumbers(c, g, context, clip, selectionMap, cursorLoc, rowHeight); - return; - } - - // paint our line number - lineNumberFieldElement.paint(c, g, 0, 0); - super.paint(c, g, context, clip, selectionMap, cursorLoc, rowHeight); - } - - private void printTextWithoutLineNumbers(JComponent c, Graphics g, PaintContext context, - Rectangle clip, FieldBackgroundColorManager selectionMap, RowColLocation cursorLoc, - int rowHeight) { - int oringalStartX = startX; - try { - // strip off the line number padding... - stripLineNumbersAndLayoutText(); - super.paint(c, g, context, clip, selectionMap, cursorLoc, rowHeight); - } - finally { - // ...restore the line number padding - reapplyLineNumbersAndLayoutText(oringalStartX); - } - } - - private void stripLineNumbersAndLayoutText() { - startX = startX - lineNumberFieldElement.getStringWidth(); - } - - private void reapplyLineNumbersAndLayoutText(int originalStartX) { - startX = originalStartX; - } - - public int getLineNumberWidth() { - return lineNumberFieldElement.getStringWidth(); - } - public int getLineNumber() { - String text = lineNumberFieldElement.getText().trim(); - return Integer.parseInt(text); + return lineNumber; } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerCallbackHandler.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerCallbackHandler.java index 0a739f83a9..91aa164dd9 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerCallbackHandler.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerCallbackHandler.java @@ -46,5 +46,5 @@ public interface DecompilerCallbackHandler { void goToFunction(Function function, boolean newWindow); - void doWheNotBusy(Callback c); + void doWhenNotBusy(Callback c); } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerController.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerController.java index 4db01954ba..58ebcb3ebc 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerController.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerController.java @@ -33,7 +33,8 @@ import ghidra.util.bean.field.AnnotatedTextFieldElement; import utility.function.Callback; /** - * Coordinates the interactions between the DecompilerProvider, DecompilerPanel, and the DecompilerManager + * Coordinates the interactions between the DecompilerProvider, DecompilerPanel, and the + * DecompilerManager */ public class DecompilerController { @@ -55,7 +56,6 @@ public class DecompilerController { new DecompilerPanel(this, options, clipboard, decompilerMgr.getTaskMonitorComponent()); decompilerPanel.setHoverMode(true); - } public DecompilerPanel getDecompilerPanel() { @@ -67,8 +67,8 @@ public class DecompilerController { //================================================================================================== /** - * Called by the provider when the provider is disposed. Once dispose is called, it should - * never be used again. + * Called by the provider when the provider is disposed. Once dispose is called, it should never + * be used again. */ public void dispose() { clearCache(); @@ -77,8 +77,8 @@ public class DecompilerController { } /** - * clears all internal state and releases all resources. Called when the provider is no - * longer visible or the currently displayed program is closed. + * clears all internal state and releases all resources. Called when the provider is no longer + * visible or the currently displayed program is closed. */ public void clear() { currentSelection = null; @@ -87,14 +87,14 @@ public class DecompilerController { } /** - * Shows the function containing the given location in the decompilerPanel. Also, positions the + * Shows the function containing the given location in the decompilerPanel. Also, positions the * decompilerPanel's cursor to the closest equivalent position. If the decompilerPanel is - * already displaying the function, then only the cursor is repositioned. To force a + * already displaying the function, then only the cursor is repositioned. To force a * re-decompile use {@link #refreshDisplay(Program, ProgramLocation, File)}. * * @param program the program for the given location - * @param location the location containing the function to be displayed and the location in - * that function to position the cursor. + * @param location the location containing the function to be displayed and the location in that + * function to position the cursor. * @param viewerPosition the viewer position */ public void display(Program program, ProgramLocation location, ViewerPosition viewerPosition) { @@ -138,6 +138,7 @@ public class DecompilerController { /** * Sets new decompiler options and triggers a new decompile. + * * @param decompilerOptions the options */ public void setOptions(DecompileOptions decompilerOptions) { @@ -159,8 +160,8 @@ public class DecompilerController { } /** - * Resets the native decompiler process. Call this method when the decompiler's view - * of a program has been invalidated, such as when a new overlay space has been added. + * Resets the native decompiler process. Call this method when the decompiler's view of a + * program has been invalidated, such as when a new overlay space has been added. */ public void resetDecompiler() { decompilerMgr.resetDecompiler(); @@ -172,6 +173,7 @@ public class DecompilerController { /** * Called by the DecompilerManager to update the currently displayed DecompileData + * * @param decompileData the new data */ public void setDecompileData(DecompileData decompileData) { @@ -199,15 +201,16 @@ public class DecompilerController { //================================================================================================== public void doWhenNotBusy(Callback c) { - callbackHandler.doWheNotBusy(c); + callbackHandler.doWhenNotBusy(c); } /** * Always decompiles the function containing the given location before positioning the * decompilerPanel's cursor to the closest equivalent position. + * * @param program the program for the given location - * @param location the location containing the function to be displayed and the location in - * that function to position the cursor. + * @param location the location containing the function to be displayed and the location in that + * function to position the cursor. * @param debugFile the debug file */ public void refreshDisplay(Program program, ProgramLocation location, File debugFile) { diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java index 292a9c76ae..6ece053a8a 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java @@ -39,8 +39,10 @@ import docking.widgets.fieldpanel.support.*; import docking.widgets.indexedscrollpane.IndexedScrollPane; import ghidra.app.decompiler.*; import ghidra.app.decompiler.component.hover.DecompilerHoverService; +import ghidra.app.decompiler.component.margin.*; import ghidra.app.plugin.core.decompile.DecompilerClipboardProvider; import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation; +import ghidra.app.util.viewer.util.ScrollpaneAlignedHorizontalLayout; import ghidra.program.model.address.*; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Program; @@ -55,7 +57,7 @@ import ghidra.util.task.SwingUpdateManager; * Class to handle the display of a decompiled function */ public class DecompilerPanel extends JPanel implements FieldMouseListener, FieldLocationListener, - FieldSelectionListener, ClangHighlightListener { + FieldSelectionListener, ClangHighlightListener, LayoutListener { private final static Color NON_FUNCTION_BACKGROUND_COLOR_DEF = new Color(220, 220, 220); @@ -64,9 +66,15 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field private final DecompilerController controller; private final DecompileOptions options; + private LineNumberDecompilerMarginProvider lineNumbersMargin; - private DecompilerFieldPanel fieldPanel; + private final DecompilerFieldPanel fieldPanel; private ClangLayoutController layoutMgr; + private final IndexedScrollPane scroller; + private final JComponent taskMonitorComponent; + + private final List marginProviders = new ArrayList<>(); + private final VerticalLayoutPixelIndexMap pixmap = new VerticalLayoutPixelIndexMap(); private HighlightFactory hlFactory; private ClangHighlightController highlightController; @@ -99,6 +107,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field this.controller = controller; this.options = options; this.clipboard = clipboard; + this.taskMonitorComponent = taskMonitorComponent; FontMetrics metrics = getFontMetrics(options); if (clipboard != null) { clipboard.setFontMetrics(metrics); @@ -109,10 +118,11 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field fieldPanel = new DecompilerFieldPanel(layoutMgr); setBackground(options.getCodeViewerBackgroundColor()); - IndexedScrollPane scroller = new IndexedScrollPane(fieldPanel); + scroller = new IndexedScrollPane(fieldPanel); fieldPanel.addFieldSelectionListener(this); fieldPanel.addFieldMouseListener(this); fieldPanel.addFieldLocationListener(this); + fieldPanel.addLayoutListener(this); decompilerHoverProvider = new DecompilerHoverProvider(); @@ -127,6 +137,10 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field setPreferredSize(new Dimension(600, 400)); setDecompileData(new EmptyDecompileData("No Function")); + + if (options.isDisplayLineNumbers()) { + addMarginProvider(lineNumbersMargin = new LineNumberDecompilerMarginProvider()); + } } public List getLines() { @@ -905,6 +919,14 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } } + @Override + public void layoutsChanged(List layouts) { + pixmap.layoutsChanged(layouts); + for (DecompilerMarginProvider element : marginProviders) { + element.setProgram(getProgram(), layoutMgr, pixmap); + } + } + private ProgramLocation getProgramLocation(Field field, FieldLocation location) { if (!(field instanceof ClangTextField)) { return null; @@ -1131,6 +1153,49 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field searchHighlightColor = decompilerOptions.getSearchHighlightColor(); highlightController.setHighlightColor(currentVariableHighlightColor); + + if (options.isDisplayLineNumbers()) { + if (lineNumbersMargin == null) { + addMarginProvider(lineNumbersMargin = new LineNumberDecompilerMarginProvider()); + } + } + else { + if (lineNumbersMargin != null) { + removeMarginProvider(lineNumbersMargin); + lineNumbersMargin = null; + } + } + + for (DecompilerMarginProvider element : marginProviders) { + element.setOptions(options); + } + } + + public void addMarginProvider(DecompilerMarginProvider provider) { + marginProviders.add(provider); + provider.setOptions(options); + provider.setProgram(getProgram(), layoutMgr, pixmap); + buildPanels(); + } + + public void removeMarginProvider(DecompilerMarginProvider provider) { + marginProviders.remove(provider); + buildPanels(); + } + + private void buildPanels() { + removeAll(); + add(buildLeftComponent(), BorderLayout.WEST); + add(scroller, BorderLayout.CENTER); + add(taskMonitorComponent, BorderLayout.SOUTH); + } + + private JComponent buildLeftComponent() { + JPanel leftPanel = new JPanel(new ScrollpaneAlignedHorizontalLayout(scroller)); + for (DecompilerMarginProvider marginProvider : marginProviders) { + leftPanel.add(marginProvider.getComponent()); + } + return leftPanel; } //================================================================================================== diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/DecompilerMarginProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/DecompilerMarginProvider.java new file mode 100644 index 0000000000..9d2bd5df3c --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/DecompilerMarginProvider.java @@ -0,0 +1,92 @@ +/* ### + * 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.decompiler.component.margin; + +import java.awt.Component; +import java.math.BigInteger; + +import javax.swing.JComponent; + +import docking.widgets.fieldpanel.LayoutModel; +import ghidra.app.decompiler.DecompileOptions; +import ghidra.app.decompiler.DecompilerMarginService; +import ghidra.program.model.listing.Program; + +/** + * A provider of a margin Swing component + * + *

+ * To add a margin to the decompiler, a client must implement this interface to provide the + * component that is actually added to the UI. For a reference implementation, see + * {@link LineNumberDecompilerMarginProvider}. + */ +public interface DecompilerMarginProvider { + + /** + * Called whenever the program, function, or layout changes + * + *

+ * The implementation should keep a reference at least to the {@code model} and the + * {@code pixmap} for later use during painting. The model provides access to the lines of + * decompiler C code. Each layout corresponds to a single line of C code. For example, the first + * line of code is rendered by the layout at index 0. The tenth is rendered by the layout at + * index 9. Rarely, a line may be wrapped by the renderer, leading to a non-uniform layout. The + * {@code pixmap} can map from a pixel's vertical position to the layout index at the same + * position in the main panel. It accounts for scrolling an non-uniformity. It is safe to assume + * the layouts render contiguous lines of C code. The recommended strategy for painting is thus: + * + *

    + *
  1. Compute the visible part of the margin needing repainting. See + * {@link JComponent#getVisibleRect()}
  2. + *
  3. Compute the layout indices for the vertical bounds of that part. See + * {@link LayoutPixelIndexMap#getIndex(int)}
  4. + *
  5. Iterate over the layouts within those bounds, inclusively.
  6. + *
  7. Compute the vertical position of each layout and paint something appropriate for its + * corresponding line. See {@link LayoutPixelIndexMap#getPixel(BigInteger)}
  8. + *
+ * + *

+ * A call to this method should cause the component to be repainted. + * + * @param program the program for the current function + * @param model the line/token model + * @param pixmap a map from pixels' y coordinates to layout index, i.e, line number + */ + void setProgram(Program program, LayoutModel model, LayoutPixelIndexMap pixmap); + + /** + * Get the Swing component implementing the actual margin, often {@code this} + * + * @return the component + */ + Component getComponent(); + + /** + * Set the options for the margin + * + *

+ * This is called at least once when the provider is added to the margin service. See + * {@link DecompilerMarginService#addMarginProvider(DecompilerMarginProvider)}. It subsequently + * called whenever a decompiler option changes. To receive other options, the provider will need + * to listen using its own mechanism. + * + *

+ * A call to this method should cause the component to be repainted. Implementors may choose to + * repaint only when certain options change. + */ + default void setOptions(DecompileOptions options) { + } +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LayoutPixelIndexMap.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LayoutPixelIndexMap.java new file mode 100644 index 0000000000..929064a426 --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LayoutPixelIndexMap.java @@ -0,0 +1,63 @@ +/* ### + * 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.decompiler.component.margin; + +import java.math.BigInteger; + +import docking.widgets.fieldpanel.LayoutModel; +import ghidra.program.model.listing.Program; + +/** + * A mapping from pixel coordinate to layout index + * + *

+ * At the moment, the only implementation provides a map from vertical position to layout. While + * this does not have to be the case, the documentation will presume the y coordinate. + */ +public interface LayoutPixelIndexMap { + /** + * Get the top of the layout with the given index + * + *

+ * Gets the minimum y coordinate of any pixel occupied by the layout having the given index. In + * essence, this maps from layout index to vertical position, relative to the main panel's + * viewport. This accounts for scrolling and non-uniform height among the layouts. + * + * @param index the index of the layout + * @return the top of the layout, relative to the main panel's viewport + */ + int getPixel(BigInteger index); + + /** + * Get the index of the layout at the given position + * + *

+ * Get the index of the layout occupying the line of pixels in the main panel having the given y + * coordinate. In essence, this maps from vertical position, relative to the main panel's + * viewport, to layout index. This accounts for scrolling and non-uniform height among the + * layouts. + * + * @implNote Clients should avoid frequent calls to this method. Even though it can be + * implemented easily in log time, an invocation for every pixel or line of pixels + * painted could still be unnecessarily expensive. It should only be necessary to call + * this once or twice per repaint. See + * {@link DecompilerMarginProvider#setProgram(Program, LayoutModel, LayoutPixelIndexMap)}. + * + * @param pixel the vertical position of the pixel, relative to the main panel's viewport + * @return the index of the layout + */ + BigInteger getIndex(int pixel); +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LineNumberDecompilerMarginProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LineNumberDecompilerMarginProvider.java new file mode 100644 index 0000000000..1f4363d965 --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/LineNumberDecompilerMarginProvider.java @@ -0,0 +1,106 @@ +/* ### + * 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.decompiler.component.margin; + +import java.awt.*; +import java.math.BigInteger; + +import javax.swing.JPanel; + +import docking.util.GraphicsUtils; +import docking.widgets.fieldpanel.LayoutModel; +import docking.widgets.fieldpanel.listener.IndexMapper; +import docking.widgets.fieldpanel.listener.LayoutModelListener; +import ghidra.app.decompiler.DecompileOptions; +import ghidra.program.model.listing.Program; + +/** + * The built-in provider for the Decompiler's line number margin + */ +public class LineNumberDecompilerMarginProvider extends JPanel + implements DecompilerMarginProvider, LayoutModelListener { + + private LayoutPixelIndexMap pixmap; + private LayoutModel model; + + @Override + public void setProgram(Program program, LayoutModel model, LayoutPixelIndexMap pixmap) { + setLayoutManager(model); + this.pixmap = pixmap; + repaint(); + } + + private void setLayoutManager(LayoutModel model) { + if (this.model == model) { + return; + } + if (this.model != null) { + this.model.removeLayoutModelListener(this); + } + this.model = model; + this.model.addLayoutModelListener(this); + setWidthForLastLine(); + if (this.model != null) { + this.model.addLayoutModelListener(this); + } + } + + @Override + public void setOptions(DecompileOptions options) { + this.setFont(options.getDefaultFont()); + setWidthForLastLine(); + repaint(); + } + + @Override + public Component getComponent() { + return this; + } + + @Override + public void modelSizeChanged(IndexMapper indexMapper) { + setWidthForLastLine(); + repaint(); + } + + @Override + public void dataChanged(BigInteger start, BigInteger end) { + repaint(); + } + + private void setWidthForLastLine() { + if (model == null) { + return; + } + int lastLine = model.getNumIndexes().intValueExact(); + int width = getFontMetrics(getFont()).stringWidth(Integer.toString(lastLine)); + setPreferredSize(new Dimension(width, 0)); + invalidate(); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + Rectangle visible = getVisibleRect(); + BigInteger startIdx = pixmap.getIndex(visible.y); + BigInteger endIdx = pixmap.getIndex(visible.y + visible.height); + int ascent = g.getFontMetrics().getMaxAscent(); + for (BigInteger i = startIdx; i.compareTo(endIdx) <= 0; i = i.add(BigInteger.ONE)) { + GraphicsUtils.drawString(this, g, i.add(BigInteger.ONE).toString(), 0, + pixmap.getPixel(i) + ascent); + } + } +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/VerticalLayoutPixelIndexMap.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/VerticalLayoutPixelIndexMap.java new file mode 100644 index 0000000000..ddf8d9bc23 --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/margin/VerticalLayoutPixelIndexMap.java @@ -0,0 +1,71 @@ +/* ### + * 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.decompiler.component.margin; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import docking.widgets.fieldpanel.support.AnchoredLayout; + +/** + * An implementation of {@link LayoutPixelIndexMap} for vertical coordinates + * + *

+ * This class implements {@link #getIndex(int)} in log time and {@link #getPixel(BigInteger)} in + * constant time. + */ +public class VerticalLayoutPixelIndexMap implements LayoutPixelIndexMap { + private BigInteger base = BigInteger.ZERO; + private int[] yPositions = new int[0]; + private int size; + + @Override + public int getPixel(BigInteger index) { + return yPositions[index.subtract(base).intValueExact()]; + } + + protected int computeOff(int pixel) { + int result = Arrays.binarySearch(yPositions, 0, size, pixel); + if (result >= 0) { + return result; + } + // result = -insertionPoint - 1, first index where acc[index] > pixel + // want index = insertionPoint - 1, index where acc[index] < pixel < acc[index+1] + // insertionPoint = -result - 1 + // index = -result - 2; + return -result - 2; + } + + @Override + public BigInteger getIndex(int pixel) { + return base.add(BigInteger.valueOf(computeOff(pixel))); + } + + public void layoutsChanged(List layouts) { + size = layouts.size(); + if (yPositions.length < size) { + yPositions = new int[size]; + } + int i = 0; + base = layouts.isEmpty() ? BigInteger.ZERO : layouts.get(0).getIndex(); + for (AnchoredLayout l : layouts) { + assert l.getIndex().subtract(base).intValueExact() == i; + yPositions[i] = l.getYPos(); + i++; + } + } +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java index e59aa35f82..f9382d2c3c 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java @@ -20,8 +20,7 @@ import java.util.*; import org.jdom.Element; import ghidra.app.CorePluginPackage; -import ghidra.app.decompiler.ClangToken; -import ghidra.app.decompiler.DecompilerHighlightService; +import ghidra.app.decompiler.*; import ghidra.app.decompiler.component.hover.DecompilerHoverService; import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; @@ -40,7 +39,6 @@ import ghidra.util.task.SwingUpdateManager; /** * Plugin for producing a high-level C interpretation of assembly functions. */ -//@formatter:off @PluginInfo( status = PluginStatus.RELEASED, packageName = CorePluginPackage.NAME, @@ -56,9 +54,7 @@ import ghidra.util.task.SwingUpdateManager; ProgramActivatedPluginEvent.class, ProgramOpenedPluginEvent.class, ProgramLocationPluginEvent.class, ProgramSelectionPluginEvent.class, ProgramClosedPluginEvent.class - } -) -//@formatter:on + }) public class DecompilePlugin extends Plugin { private PrimaryDecompilerProvider connectedProvider; @@ -69,9 +65,8 @@ public class DecompilePlugin extends Plugin { private ProgramSelection currentSelection; /** - * Delay location changes to allow location events to settle down. - * This happens when a readDataState occurs when a tool is restored - * or when switching program tabs. + * Delay location changes to allow location events to settle down. This happens when a + * readDataState occurs when a tool is restored or when switching program tabs. */ SwingUpdateManager delayedLocationUpdateMgr = new SwingUpdateManager(200, 200, () -> { if (currentLocation == null) { @@ -96,6 +91,8 @@ public class DecompilePlugin extends Plugin { private void registerServices() { registerServiceProvided(DecompilerHighlightService.class, connectedProvider); + // Allow pluggable margin providers for disconnected providers? + registerServiceProvided(DecompilerMarginService.class, connectedProvider); } @Override 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 015dcaa50e..1851b9f2ee 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 @@ -209,7 +209,7 @@ public class DecompilerClipboardProvider extends ByteCopier LayoutModel model = provider.getDecompilerPanel().getLayoutModel(); Layout layout = model.getLayout(BigInteger.valueOf(lineNumber)); ClangTextField field = (ClangTextField) layout.getField(0); - int numSpaces = (field.getStartX() - field.getLineNumberWidth()) / spaceCharWidthInPixels; + int numSpaces = field.getStartX() / spaceCharWidthInPixels; for (int i = 0; i < numSpaces; i++) { buffer.append(' '); } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java index fd60ea164c..a5bcbf8ac5 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,6 +30,7 @@ import docking.widgets.fieldpanel.support.ViewerPosition; import ghidra.GhidraOptions; import ghidra.app.decompiler.*; import ghidra.app.decompiler.component.*; +import ghidra.app.decompiler.component.margin.DecompilerMarginProvider; import ghidra.app.nav.*; import ghidra.app.plugin.core.decompile.actions.*; import ghidra.app.services.*; @@ -55,7 +56,7 @@ import utility.function.Callback; public class DecompilerProvider extends NavigatableComponentProviderAdapter implements DomainObjectListener, OptionsChangeListener, DecompilerCallbackHandler, - DecompilerHighlightService { + DecompilerHighlightService, DecompilerMarginService { private static final String OPTIONS_TITLE = "Decompiler"; @@ -385,6 +386,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter /** * Sets the current program and adds/removes itself as a domainObjectListener + * * @param newProgram the new program or null to clear out the current program. */ void doSetProgram(Program newProgram) { @@ -422,9 +424,10 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter } /** - * sets the current location for this provider. If the provider is not visible, it does not - * pass it on to the controller. When the component is later shown, the current location - * will then be passed to the controller. + * sets the current location for this provider. If the provider is not visible, it does not pass + * it on to the controller. When the component is later shown, the current location will then be + * passed to the controller. + * * @param loc the location to compile and set the cursor. * @param viewerPosition if non-null the position at which to scroll the view. */ @@ -471,8 +474,8 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter } /** - * Returns a string that shows the current line with the field under the cursor in between - * '[]' chars. + * Returns a string that shows the current line with the field under the cursor in between '[]' + * chars. * * @return the string */ @@ -648,7 +651,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter } @Override - public void doWheNotBusy(Callback c) { + public void doWhenNotBusy(Callback c) { followUpWork.offer(c); followUpWorkUpdater.update(); } @@ -678,7 +681,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter // transfer any state after the new decompiler is initialized DecompilerPanel newPanel = newProvider.getDecompilerPanel(); - newProvider.doWheNotBusy(() -> { + newProvider.doWhenNotBusy(() -> { newPanel.setViewerPosition(myViewPosition); newPanel.cloneHighlights(myPanel); }); @@ -1113,4 +1116,14 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter void handleTokenRenamed(ClangToken tokenAtCursor, String newName) { controller.getDecompilerPanel().tokenRenamed(tokenAtCursor, newName); } + + @Override + public void addMarginProvider(DecompilerMarginProvider provider) { + getDecompilerPanel().addMarginProvider(provider); + } + + @Override + public void removeMarginProvider(DecompilerMarginProvider provider) { + getDecompilerPanel().removeMarginProvider(provider); + } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ConvertConstantTask.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ConvertConstantTask.java index 621b552e89..f9639aa18a 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ConvertConstantTask.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/ConvertConstantTask.java @@ -310,7 +310,7 @@ public class ConvertConstantTask implements Callback { catch (InterruptedException e) { return; } - context.getComponentProvider().doWheNotBusy(this); + context.getComponentProvider().doWhenNotBusy(this); } else { applyPrimaryEquate(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/LayoutModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/LayoutModel.java index 81af930229..144ce33f00 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/LayoutModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/LayoutModel.java @@ -25,7 +25,7 @@ import docking.widgets.fieldpanel.listener.LayoutModelListener; * using a BigFieldPanel. */ -public interface LayoutModel { +public interface LayoutModel extends Iterable { /** * Returns true if every index returns a non-null layout and all the layouts @@ -68,6 +68,7 @@ public interface LayoutModel { * * @return new iterator */ + @Override public default LayoutModelIterator iterator() { return new LayoutModelIterator(this); }