GP-2446: Add margin provider to decompiler. Port line numbers to it.

This commit is contained in:
Dan 2022-10-04 14:42:41 -04:00
parent 7e24c986ad
commit e72aa6e039
16 changed files with 520 additions and 259 deletions

View file

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

View file

@ -159,7 +159,7 @@ public class CDisplayPanel extends JPanel implements DecompilerCallbackHandler {
}
@Override
public void doWheNotBusy(Callback c) {
public void doWhenNotBusy(Callback c) {
// stub
}

View file

@ -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<LayoutModelListener> listeners;
private Color[] syntax_color; // Foreground colors.
private Color[] syntaxColor; // Foreground colors.
private BigInteger numIndexes = BigInteger.ZERO;
private ArrayList<ClangLine> 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<ClangToken> 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;

View file

@ -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<ClangToken> 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<ClangToken> 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<ClangToken> 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 <b>inside</b> 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 <b>inside</b> 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;
}
}

View file

@ -46,5 +46,5 @@ public interface DecompilerCallbackHandler {
void goToFunction(Function function, boolean newWindow);
void doWheNotBusy(Callback c);
void doWhenNotBusy(Callback c);
}

View file

@ -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) {

View file

@ -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<DecompilerMarginProvider> 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<ClangLine> getLines() {
@ -905,6 +919,14 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
}
}
@Override
public void layoutsChanged(List<AnchoredLayout> 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;
}
//==================================================================================================

View file

@ -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
*
* <p>
* 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
*
* <p>
* 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:
*
* <ol>
* <li>Compute the visible part of the margin needing repainting. See
* {@link JComponent#getVisibleRect()}</li>
* <li>Compute the layout indices for the vertical bounds of that part. See
* {@link LayoutPixelIndexMap#getIndex(int)}</li>
* <li>Iterate over the layouts within those bounds, inclusively.</li>
* <li>Compute the vertical position of each layout and paint something appropriate for its
* corresponding line. See {@link LayoutPixelIndexMap#getPixel(BigInteger)}</li>
* </ol>
*
* <p>
* 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
*
* <p>
* 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.
*
* <p>
* 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) {
}
}

View file

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

View file

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

View file

@ -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
*
* <p>
* 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<AnchoredLayout> 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++;
}
}
}

View file

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

View file

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

View file

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

View file

@ -310,7 +310,7 @@ public class ConvertConstantTask implements Callback {
catch (InterruptedException e) {
return;
}
context.getComponentProvider().doWheNotBusy(this);
context.getComponentProvider().doWhenNotBusy(this);
}
else {
applyPrimaryEquate();

View file

@ -25,7 +25,7 @@ import docking.widgets.fieldpanel.listener.LayoutModelListener;
* using a BigFieldPanel.
*/
public interface LayoutModel {
public interface LayoutModel extends Iterable<Layout> {
/**
* 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);
}