mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-08-28 13:30:21 +00:00
GP-2446: Add margin provider to decompiler. Port line numbers to it.
This commit is contained in:
parent
7e24c986ad
commit
e72aa6e039
|
@ -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);
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ public class CDisplayPanel extends JPanel implements DecompilerCallbackHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doWheNotBusy(Callback c) {
|
public void doWhenNotBusy(Callback c) {
|
||||||
// stub
|
// stub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,6 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.*;
|
import java.util.regex.*;
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import docking.widgets.SearchLocation;
|
import docking.widgets.SearchLocation;
|
||||||
|
@ -47,10 +45,7 @@ import ghidra.util.Msg;
|
||||||
*/
|
*/
|
||||||
public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
|
|
||||||
private final ClangFieldElement EMPTY_LINE_NUMBER_SPACER;
|
|
||||||
|
|
||||||
private int maxWidth;
|
private int maxWidth;
|
||||||
private int lineNumberFieldWidth;
|
|
||||||
private int indentWidth;
|
private int indentWidth;
|
||||||
private DecompileOptions options;
|
private DecompileOptions options;
|
||||||
private DecompilerPanel decompilerPanel;
|
private DecompilerPanel decompilerPanel;
|
||||||
|
@ -59,26 +54,19 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
private FontMetrics metrics;
|
private FontMetrics metrics;
|
||||||
private HighlightFactory hlFactory;
|
private HighlightFactory hlFactory;
|
||||||
private ArrayList<LayoutModelListener> listeners;
|
private ArrayList<LayoutModelListener> listeners;
|
||||||
private Color[] syntax_color; // Foreground colors.
|
private Color[] syntaxColor; // Foreground colors.
|
||||||
private BigInteger numIndexes = BigInteger.ZERO;
|
private BigInteger numIndexes = BigInteger.ZERO;
|
||||||
private ArrayList<ClangLine> lines = new ArrayList<>();
|
private ArrayList<ClangLine> lines = new ArrayList<>();
|
||||||
|
|
||||||
private boolean showLineNumbers = true;
|
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,
|
public ClangLayoutController(DecompileOptions opt, DecompilerPanel decompilerPanel,
|
||||||
FontMetrics met, HighlightFactory hlFactory) {
|
FontMetrics met, HighlightFactory hlFactory) {
|
||||||
options = opt;
|
options = opt;
|
||||||
this.decompilerPanel = decompilerPanel;
|
this.decompilerPanel = decompilerPanel;
|
||||||
syntax_color = new Color[ClangToken.MAX_COLOR];
|
syntaxColor = new Color[ClangToken.MAX_COLOR];
|
||||||
metrics = met;
|
metrics = met;
|
||||||
this.hlFactory = hlFactory;
|
this.hlFactory = hlFactory;
|
||||||
EMPTY_LINE_NUMBER_SPACER = createEmptyLineNumberSpacer();
|
|
||||||
listeners = new ArrayList<>();
|
listeners = new ArrayList<>();
|
||||||
buildLayouts(null, null, null, false);
|
buildLayouts(null, null, null, false);
|
||||||
}
|
}
|
||||||
|
@ -94,7 +82,7 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dimension getPreferredViewSize() {
|
public Dimension getPreferredViewSize() {
|
||||||
return new Dimension(maxWidth + lineNumberFieldWidth, 500);
|
return new Dimension(maxWidth, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -179,15 +167,11 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
boolean paintLineNumbers) {
|
boolean paintLineNumbers) {
|
||||||
List<ClangToken> tokens = line.getAllTokens();
|
List<ClangToken> tokens = line.getAllTokens();
|
||||||
|
|
||||||
ClangFieldElement lineNumberFieldElement =
|
|
||||||
createLineNumberFieldElement(line, lineCount, paintLineNumbers);
|
|
||||||
|
|
||||||
FieldElement[] elements = createFieldElementsForLine(tokens);
|
FieldElement[] elements = createFieldElementsForLine(tokens);
|
||||||
|
|
||||||
int indent = line.getIndent() * indentWidth;
|
int indent = line.getIndent() * indentWidth;
|
||||||
int lineNumberWidth = lineNumberFieldElement.getStringWidth();
|
int updatedMaxWidth = maxWidth;
|
||||||
int updatedMaxWidth = maxWidth + lineNumberWidth;
|
return new ClangTextField(tokens, elements, indent, line.getLineNumber(), updatedMaxWidth,
|
||||||
return new ClangTextField(tokens, elements, lineNumberFieldElement, indent, updatedMaxWidth,
|
|
||||||
hlFactory);
|
hlFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +181,7 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
int columnPosition = 0;
|
int columnPosition = 0;
|
||||||
for (int i = 0; i < tokens.size(); ++i) {
|
for (int i = 0; i < tokens.size(); ++i) {
|
||||||
ClangToken token = tokens.get(i);
|
ClangToken token = tokens.get(i);
|
||||||
Color color = syntax_color[token.getSyntaxType()];
|
Color color = syntaxColor[token.getSyntaxType()];
|
||||||
if (token instanceof ClangCommentToken) {
|
if (token instanceof ClangCommentToken) {
|
||||||
AttributedString prototype = new AttributedString("prototype", color, metrics);
|
AttributedString prototype = new AttributedString("prototype", color, metrics);
|
||||||
Program program = decompilerPanel.getProgram();
|
Program program = decompilerPanel.getProgram();
|
||||||
|
@ -214,32 +198,22 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
return elements;
|
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
|
* Update to the current Decompiler display options
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
// ignoring the deprecated call for toolkit
|
// ignoring the deprecated call for toolkit
|
||||||
private void updateOptions() {
|
private void updateOptions() {
|
||||||
syntax_color[ClangToken.KEYWORD_COLOR] = options.getKeywordColor();
|
syntaxColor[ClangToken.KEYWORD_COLOR] = options.getKeywordColor();
|
||||||
syntax_color[ClangToken.TYPE_COLOR] = options.getTypeColor();
|
syntaxColor[ClangToken.TYPE_COLOR] = options.getTypeColor();
|
||||||
syntax_color[ClangToken.FUNCTION_COLOR] = options.getFunctionColor();
|
syntaxColor[ClangToken.FUNCTION_COLOR] = options.getFunctionColor();
|
||||||
syntax_color[ClangToken.COMMENT_COLOR] = options.getCommentColor();
|
syntaxColor[ClangToken.COMMENT_COLOR] = options.getCommentColor();
|
||||||
syntax_color[ClangToken.VARIABLE_COLOR] = options.getVariableColor();
|
syntaxColor[ClangToken.VARIABLE_COLOR] = options.getVariableColor();
|
||||||
syntax_color[ClangToken.CONST_COLOR] = options.getConstantColor();
|
syntaxColor[ClangToken.CONST_COLOR] = options.getConstantColor();
|
||||||
syntax_color[ClangToken.PARAMETER_COLOR] = options.getParameterColor();
|
syntaxColor[ClangToken.PARAMETER_COLOR] = options.getParameterColor();
|
||||||
syntax_color[ClangToken.GLOBAL_COLOR] = options.getGlobalColor();
|
syntaxColor[ClangToken.GLOBAL_COLOR] = options.getGlobalColor();
|
||||||
syntax_color[ClangToken.DEFAULT_COLOR] = options.getDefaultColor();
|
syntaxColor[ClangToken.DEFAULT_COLOR] = options.getDefaultColor();
|
||||||
syntax_color[ClangToken.ERROR_COLOR] = options.getErrorColor();
|
syntaxColor[ClangToken.ERROR_COLOR] = options.getErrorColor();
|
||||||
|
|
||||||
// setting the metrics here will indirectly trigger the new font to be used deeper in
|
// 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)
|
// 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);
|
metrics = Toolkit.getDefaultToolkit().getFontMetrics(font);
|
||||||
indentWidth = metrics.stringWidth(PrettyPrinter.INDENT_STRING);
|
indentWidth = metrics.stringWidth(PrettyPrinter.INDENT_STRING);
|
||||||
maxWidth = indentWidth * options.getMaxWidth();
|
maxWidth = indentWidth * options.getMaxWidth();
|
||||||
lineNumberFieldWidth = 0;
|
|
||||||
|
|
||||||
showLineNumbers = options.isDisplayLineNumbers();
|
showLineNumbers = options.isDisplayLineNumbers();
|
||||||
}
|
}
|
||||||
|
@ -264,11 +237,6 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
fieldList = new Field[lineCount]; // One field for each "C" line
|
fieldList = new Field[lineCount]; // One field for each "C" line
|
||||||
numIndexes = BigInteger.valueOf(lineCount);
|
numIndexes = BigInteger.valueOf(lineCount);
|
||||||
|
|
||||||
lineNumberFieldWidth = 0;
|
|
||||||
if (showLineNumbers && !isError) {
|
|
||||||
lineNumberFieldWidth = LineNumberFieldElement.getFieldWidth(metrics, lineCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < lineCount; ++i) {
|
for (int i = 0; i < lineCount; ++i) {
|
||||||
ClangLine oneLine = lines.get(i);
|
ClangLine oneLine = lines.get(i);
|
||||||
fieldList[i] = createTextFieldForLine(oneLine, lineCount, showLineNumbers);
|
fieldList[i] = createTextFieldForLine(oneLine, lineCount, showLineNumbers);
|
||||||
|
@ -600,81 +568,6 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
|
||||||
// Inner Classes
|
// 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 class FieldNumberColumnPair {
|
||||||
private final int fieldNumber;
|
private final int fieldNumber;
|
||||||
private final int column;
|
private final int column;
|
||||||
|
|
|
@ -15,68 +15,32 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.app.decompiler.component;
|
package ghidra.app.decompiler.component;
|
||||||
|
|
||||||
import java.awt.Graphics;
|
|
||||||
import java.awt.Rectangle;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
|
|
||||||
import docking.widgets.fieldpanel.field.*;
|
import docking.widgets.fieldpanel.field.*;
|
||||||
import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager;
|
import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
import docking.widgets.fieldpanel.internal.PaintContext;
|
import docking.widgets.fieldpanel.support.HighlightFactory;
|
||||||
import docking.widgets.fieldpanel.support.*;
|
|
||||||
import ghidra.app.decompiler.ClangToken;
|
import ghidra.app.decompiler.ClangToken;
|
||||||
|
|
||||||
public class ClangTextField extends WrappingVerticalLayoutTextField {
|
public class ClangTextField extends WrappingVerticalLayoutTextField {
|
||||||
|
|
||||||
private List<ClangToken> tokenList;
|
private List<ClangToken> tokenList;
|
||||||
private FieldElement lineNumberFieldElement;
|
private final int lineNumber;
|
||||||
|
|
||||||
private static FieldElement createSingleLineElement(FieldElement[] textElements) {
|
private static FieldElement createSingleLineElement(FieldElement[] textElements) {
|
||||||
return new CompositeFieldElement(textElements);
|
return new CompositeFieldElement(textElements);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public ClangTextField(List<ClangToken> tokenList, FieldElement[] fieldElements, int x,
|
||||||
* Calculates the offset of the x position for the given line number element. The line
|
int lineNumber, int width, HighlightFactory hlFactory) {
|
||||||
* numbers of the decompiler appear to the left of the data and thus offset the actual data
|
super(createSingleLineElement(fieldElements), x, width - x, 30, hlFactory, false);
|
||||||
* 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);
|
|
||||||
this.tokenList = tokenList;
|
this.tokenList = tokenList;
|
||||||
this.lineNumberFieldElement = lineNumberFieldElement;
|
this.lineNumber = lineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the C language token at the indicated location.
|
* Gets the C language token at the indicated location.
|
||||||
|
*
|
||||||
* @param loc the field location
|
* @param loc the field location
|
||||||
* @return the token
|
* @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
|
* Returns the token that is completely after the token that contains the given column location.
|
||||||
* location. In this case, 'contains' means any position <b>inside</b> of a token, but
|
* In this case, 'contains' means any position <b>inside</b> of a token, but not at the
|
||||||
* not at the beginning. So, if the column location is in the middle of a
|
* beginning. So, if the column location is in the middle of a token, it will return the index
|
||||||
* token, it will return the index of next token. But if the column location is at
|
* of next token. But if the column location is at the beginning (just before the start) of a
|
||||||
* the beginning (just before the start) of a token, it will return the index of that token.
|
* token, it will return the index of that token.
|
||||||
*
|
*
|
||||||
* @param location containing the column at which to beginning searching
|
* @param location containing the column at which to beginning searching
|
||||||
* @return the next token starting after the given column
|
* @return the next token starting after the given column
|
||||||
|
@ -156,51 +120,7 @@ public class ClangTextField extends WrappingVerticalLayoutTextField {
|
||||||
return tokenList;
|
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() {
|
public int getLineNumber() {
|
||||||
String text = lineNumberFieldElement.getText().trim();
|
return lineNumber;
|
||||||
return Integer.parseInt(text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,5 +46,5 @@ public interface DecompilerCallbackHandler {
|
||||||
|
|
||||||
void goToFunction(Function function, boolean newWindow);
|
void goToFunction(Function function, boolean newWindow);
|
||||||
|
|
||||||
void doWheNotBusy(Callback c);
|
void doWhenNotBusy(Callback c);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ import ghidra.util.bean.field.AnnotatedTextFieldElement;
|
||||||
import utility.function.Callback;
|
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 {
|
public class DecompilerController {
|
||||||
|
@ -55,7 +56,6 @@ public class DecompilerController {
|
||||||
new DecompilerPanel(this, options, clipboard, decompilerMgr.getTaskMonitorComponent());
|
new DecompilerPanel(this, options, clipboard, decompilerMgr.getTaskMonitorComponent());
|
||||||
|
|
||||||
decompilerPanel.setHoverMode(true);
|
decompilerPanel.setHoverMode(true);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DecompilerPanel getDecompilerPanel() {
|
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
|
* Called by the provider when the provider is disposed. Once dispose is called, it should never
|
||||||
* never be used again.
|
* be used again.
|
||||||
*/
|
*/
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
clearCache();
|
clearCache();
|
||||||
|
@ -77,8 +77,8 @@ public class DecompilerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* clears all internal state and releases all resources. Called when the provider is no
|
* clears all internal state and releases all resources. Called when the provider is no longer
|
||||||
* longer visible or the currently displayed program is closed.
|
* visible or the currently displayed program is closed.
|
||||||
*/
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
currentSelection = null;
|
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
|
* 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)}.
|
* re-decompile use {@link #refreshDisplay(Program, ProgramLocation, File)}.
|
||||||
*
|
*
|
||||||
* @param program the program for the given location
|
* @param program the program for the given location
|
||||||
* @param location the location containing the function to be displayed and the location in
|
* @param location the location containing the function to be displayed and the location in that
|
||||||
* that function to position the cursor.
|
* function to position the cursor.
|
||||||
* @param viewerPosition the viewer position
|
* @param viewerPosition the viewer position
|
||||||
*/
|
*/
|
||||||
public void display(Program program, ProgramLocation location, ViewerPosition viewerPosition) {
|
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.
|
* Sets new decompiler options and triggers a new decompile.
|
||||||
|
*
|
||||||
* @param decompilerOptions the options
|
* @param decompilerOptions the options
|
||||||
*/
|
*/
|
||||||
public void setOptions(DecompileOptions decompilerOptions) {
|
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
|
* Resets the native decompiler process. Call this method when the decompiler's view of a
|
||||||
* of a program has been invalidated, such as when a new overlay space has been added.
|
* program has been invalidated, such as when a new overlay space has been added.
|
||||||
*/
|
*/
|
||||||
public void resetDecompiler() {
|
public void resetDecompiler() {
|
||||||
decompilerMgr.resetDecompiler();
|
decompilerMgr.resetDecompiler();
|
||||||
|
@ -172,6 +173,7 @@ public class DecompilerController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the DecompilerManager to update the currently displayed DecompileData
|
* Called by the DecompilerManager to update the currently displayed DecompileData
|
||||||
|
*
|
||||||
* @param decompileData the new data
|
* @param decompileData the new data
|
||||||
*/
|
*/
|
||||||
public void setDecompileData(DecompileData decompileData) {
|
public void setDecompileData(DecompileData decompileData) {
|
||||||
|
@ -199,15 +201,16 @@ public class DecompilerController {
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
||||||
public void doWhenNotBusy(Callback c) {
|
public void doWhenNotBusy(Callback c) {
|
||||||
callbackHandler.doWheNotBusy(c);
|
callbackHandler.doWhenNotBusy(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Always decompiles the function containing the given location before positioning the
|
* Always decompiles the function containing the given location before positioning the
|
||||||
* decompilerPanel's cursor to the closest equivalent position.
|
* decompilerPanel's cursor to the closest equivalent position.
|
||||||
|
*
|
||||||
* @param program the program for the given location
|
* @param program the program for the given location
|
||||||
* @param location the location containing the function to be displayed and the location in
|
* @param location the location containing the function to be displayed and the location in that
|
||||||
* that function to position the cursor.
|
* function to position the cursor.
|
||||||
* @param debugFile the debug file
|
* @param debugFile the debug file
|
||||||
*/
|
*/
|
||||||
public void refreshDisplay(Program program, ProgramLocation location, File debugFile) {
|
public void refreshDisplay(Program program, ProgramLocation location, File debugFile) {
|
||||||
|
|
|
@ -39,8 +39,10 @@ import docking.widgets.fieldpanel.support.*;
|
||||||
import docking.widgets.indexedscrollpane.IndexedScrollPane;
|
import docking.widgets.indexedscrollpane.IndexedScrollPane;
|
||||||
import ghidra.app.decompiler.*;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.decompiler.component.hover.DecompilerHoverService;
|
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.DecompilerClipboardProvider;
|
||||||
import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation;
|
import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation;
|
||||||
|
import ghidra.app.util.viewer.util.ScrollpaneAlignedHorizontalLayout;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.program.model.listing.Function;
|
import ghidra.program.model.listing.Function;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
|
@ -55,7 +57,7 @@ import ghidra.util.task.SwingUpdateManager;
|
||||||
* Class to handle the display of a decompiled function
|
* Class to handle the display of a decompiled function
|
||||||
*/
|
*/
|
||||||
public class DecompilerPanel extends JPanel implements FieldMouseListener, FieldLocationListener,
|
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);
|
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 DecompilerController controller;
|
||||||
private final DecompileOptions options;
|
private final DecompileOptions options;
|
||||||
|
private LineNumberDecompilerMarginProvider lineNumbersMargin;
|
||||||
|
|
||||||
private DecompilerFieldPanel fieldPanel;
|
private final DecompilerFieldPanel fieldPanel;
|
||||||
private ClangLayoutController layoutMgr;
|
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 HighlightFactory hlFactory;
|
||||||
private ClangHighlightController highlightController;
|
private ClangHighlightController highlightController;
|
||||||
|
@ -99,6 +107,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.clipboard = clipboard;
|
this.clipboard = clipboard;
|
||||||
|
this.taskMonitorComponent = taskMonitorComponent;
|
||||||
FontMetrics metrics = getFontMetrics(options);
|
FontMetrics metrics = getFontMetrics(options);
|
||||||
if (clipboard != null) {
|
if (clipboard != null) {
|
||||||
clipboard.setFontMetrics(metrics);
|
clipboard.setFontMetrics(metrics);
|
||||||
|
@ -109,10 +118,11 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
||||||
fieldPanel = new DecompilerFieldPanel(layoutMgr);
|
fieldPanel = new DecompilerFieldPanel(layoutMgr);
|
||||||
setBackground(options.getCodeViewerBackgroundColor());
|
setBackground(options.getCodeViewerBackgroundColor());
|
||||||
|
|
||||||
IndexedScrollPane scroller = new IndexedScrollPane(fieldPanel);
|
scroller = new IndexedScrollPane(fieldPanel);
|
||||||
fieldPanel.addFieldSelectionListener(this);
|
fieldPanel.addFieldSelectionListener(this);
|
||||||
fieldPanel.addFieldMouseListener(this);
|
fieldPanel.addFieldMouseListener(this);
|
||||||
fieldPanel.addFieldLocationListener(this);
|
fieldPanel.addFieldLocationListener(this);
|
||||||
|
fieldPanel.addLayoutListener(this);
|
||||||
|
|
||||||
decompilerHoverProvider = new DecompilerHoverProvider();
|
decompilerHoverProvider = new DecompilerHoverProvider();
|
||||||
|
|
||||||
|
@ -127,6 +137,10 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
||||||
|
|
||||||
setPreferredSize(new Dimension(600, 400));
|
setPreferredSize(new Dimension(600, 400));
|
||||||
setDecompileData(new EmptyDecompileData("No Function"));
|
setDecompileData(new EmptyDecompileData("No Function"));
|
||||||
|
|
||||||
|
if (options.isDisplayLineNumbers()) {
|
||||||
|
addMarginProvider(lineNumbersMargin = new LineNumberDecompilerMarginProvider());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ClangLine> getLines() {
|
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) {
|
private ProgramLocation getProgramLocation(Field field, FieldLocation location) {
|
||||||
if (!(field instanceof ClangTextField)) {
|
if (!(field instanceof ClangTextField)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1131,6 +1153,49 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
||||||
searchHighlightColor = decompilerOptions.getSearchHighlightColor();
|
searchHighlightColor = decompilerOptions.getSearchHighlightColor();
|
||||||
|
|
||||||
highlightController.setHighlightColor(currentVariableHighlightColor);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,7 @@ import java.util.*;
|
||||||
import org.jdom.Element;
|
import org.jdom.Element;
|
||||||
|
|
||||||
import ghidra.app.CorePluginPackage;
|
import ghidra.app.CorePluginPackage;
|
||||||
import ghidra.app.decompiler.ClangToken;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.decompiler.DecompilerHighlightService;
|
|
||||||
import ghidra.app.decompiler.component.hover.DecompilerHoverService;
|
import ghidra.app.decompiler.component.hover.DecompilerHoverService;
|
||||||
import ghidra.app.events.*;
|
import ghidra.app.events.*;
|
||||||
import ghidra.app.plugin.PluginCategoryNames;
|
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.
|
* Plugin for producing a high-level C interpretation of assembly functions.
|
||||||
*/
|
*/
|
||||||
//@formatter:off
|
|
||||||
@PluginInfo(
|
@PluginInfo(
|
||||||
status = PluginStatus.RELEASED,
|
status = PluginStatus.RELEASED,
|
||||||
packageName = CorePluginPackage.NAME,
|
packageName = CorePluginPackage.NAME,
|
||||||
|
@ -56,9 +54,7 @@ import ghidra.util.task.SwingUpdateManager;
|
||||||
ProgramActivatedPluginEvent.class, ProgramOpenedPluginEvent.class,
|
ProgramActivatedPluginEvent.class, ProgramOpenedPluginEvent.class,
|
||||||
ProgramLocationPluginEvent.class, ProgramSelectionPluginEvent.class,
|
ProgramLocationPluginEvent.class, ProgramSelectionPluginEvent.class,
|
||||||
ProgramClosedPluginEvent.class
|
ProgramClosedPluginEvent.class
|
||||||
}
|
})
|
||||||
)
|
|
||||||
//@formatter:on
|
|
||||||
public class DecompilePlugin extends Plugin {
|
public class DecompilePlugin extends Plugin {
|
||||||
|
|
||||||
private PrimaryDecompilerProvider connectedProvider;
|
private PrimaryDecompilerProvider connectedProvider;
|
||||||
|
@ -69,9 +65,8 @@ public class DecompilePlugin extends Plugin {
|
||||||
private ProgramSelection currentSelection;
|
private ProgramSelection currentSelection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delay location changes to allow location events to settle down.
|
* Delay location changes to allow location events to settle down. This happens when a
|
||||||
* This happens when a readDataState occurs when a tool is restored
|
* readDataState occurs when a tool is restored or when switching program tabs.
|
||||||
* or when switching program tabs.
|
|
||||||
*/
|
*/
|
||||||
SwingUpdateManager delayedLocationUpdateMgr = new SwingUpdateManager(200, 200, () -> {
|
SwingUpdateManager delayedLocationUpdateMgr = new SwingUpdateManager(200, 200, () -> {
|
||||||
if (currentLocation == null) {
|
if (currentLocation == null) {
|
||||||
|
@ -96,6 +91,8 @@ public class DecompilePlugin extends Plugin {
|
||||||
|
|
||||||
private void registerServices() {
|
private void registerServices() {
|
||||||
registerServiceProvided(DecompilerHighlightService.class, connectedProvider);
|
registerServiceProvided(DecompilerHighlightService.class, connectedProvider);
|
||||||
|
// Allow pluggable margin providers for disconnected providers?
|
||||||
|
registerServiceProvided(DecompilerMarginService.class, connectedProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -209,7 +209,7 @@ public class DecompilerClipboardProvider extends ByteCopier
|
||||||
LayoutModel model = provider.getDecompilerPanel().getLayoutModel();
|
LayoutModel model = provider.getDecompilerPanel().getLayoutModel();
|
||||||
Layout layout = model.getLayout(BigInteger.valueOf(lineNumber));
|
Layout layout = model.getLayout(BigInteger.valueOf(lineNumber));
|
||||||
ClangTextField field = (ClangTextField) layout.getField(0);
|
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++) {
|
for (int i = 0; i < numSpaces; i++) {
|
||||||
buffer.append(' ');
|
buffer.append(' ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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.GhidraOptions;
|
||||||
import ghidra.app.decompiler.*;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.decompiler.component.*;
|
import ghidra.app.decompiler.component.*;
|
||||||
|
import ghidra.app.decompiler.component.margin.DecompilerMarginProvider;
|
||||||
import ghidra.app.nav.*;
|
import ghidra.app.nav.*;
|
||||||
import ghidra.app.plugin.core.decompile.actions.*;
|
import ghidra.app.plugin.core.decompile.actions.*;
|
||||||
import ghidra.app.services.*;
|
import ghidra.app.services.*;
|
||||||
|
@ -55,7 +56,7 @@ import utility.function.Callback;
|
||||||
|
|
||||||
public class DecompilerProvider extends NavigatableComponentProviderAdapter
|
public class DecompilerProvider extends NavigatableComponentProviderAdapter
|
||||||
implements DomainObjectListener, OptionsChangeListener, DecompilerCallbackHandler,
|
implements DomainObjectListener, OptionsChangeListener, DecompilerCallbackHandler,
|
||||||
DecompilerHighlightService {
|
DecompilerHighlightService, DecompilerMarginService {
|
||||||
|
|
||||||
private static final String OPTIONS_TITLE = "Decompiler";
|
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
|
* Sets the current program and adds/removes itself as a domainObjectListener
|
||||||
|
*
|
||||||
* @param newProgram the new program or null to clear out the current program.
|
* @param newProgram the new program or null to clear out the current program.
|
||||||
*/
|
*/
|
||||||
void doSetProgram(Program newProgram) {
|
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
|
* sets the current location for this provider. If the provider is not visible, it does not pass
|
||||||
* pass it on to the controller. When the component is later shown, the current location
|
* it on to the controller. When the component is later shown, the current location will then be
|
||||||
* will then be passed to the controller.
|
* passed to the controller.
|
||||||
|
*
|
||||||
* @param loc the location to compile and set the cursor.
|
* @param loc the location to compile and set the cursor.
|
||||||
* @param viewerPosition if non-null the position at which to scroll the view.
|
* @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
|
* Returns a string that shows the current line with the field under the cursor in between '[]'
|
||||||
* '[]' chars.
|
* chars.
|
||||||
*
|
*
|
||||||
* @return the string
|
* @return the string
|
||||||
*/
|
*/
|
||||||
|
@ -648,7 +651,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doWheNotBusy(Callback c) {
|
public void doWhenNotBusy(Callback c) {
|
||||||
followUpWork.offer(c);
|
followUpWork.offer(c);
|
||||||
followUpWorkUpdater.update();
|
followUpWorkUpdater.update();
|
||||||
}
|
}
|
||||||
|
@ -678,7 +681,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
|
||||||
|
|
||||||
// transfer any state after the new decompiler is initialized
|
// transfer any state after the new decompiler is initialized
|
||||||
DecompilerPanel newPanel = newProvider.getDecompilerPanel();
|
DecompilerPanel newPanel = newProvider.getDecompilerPanel();
|
||||||
newProvider.doWheNotBusy(() -> {
|
newProvider.doWhenNotBusy(() -> {
|
||||||
newPanel.setViewerPosition(myViewPosition);
|
newPanel.setViewerPosition(myViewPosition);
|
||||||
newPanel.cloneHighlights(myPanel);
|
newPanel.cloneHighlights(myPanel);
|
||||||
});
|
});
|
||||||
|
@ -1113,4 +1116,14 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
|
||||||
void handleTokenRenamed(ClangToken tokenAtCursor, String newName) {
|
void handleTokenRenamed(ClangToken tokenAtCursor, String newName) {
|
||||||
controller.getDecompilerPanel().tokenRenamed(tokenAtCursor, newName);
|
controller.getDecompilerPanel().tokenRenamed(tokenAtCursor, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMarginProvider(DecompilerMarginProvider provider) {
|
||||||
|
getDecompilerPanel().addMarginProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeMarginProvider(DecompilerMarginProvider provider) {
|
||||||
|
getDecompilerPanel().removeMarginProvider(provider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,7 +310,7 @@ public class ConvertConstantTask implements Callback {
|
||||||
catch (InterruptedException e) {
|
catch (InterruptedException e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.getComponentProvider().doWheNotBusy(this);
|
context.getComponentProvider().doWhenNotBusy(this);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
applyPrimaryEquate();
|
applyPrimaryEquate();
|
||||||
|
|
|
@ -25,7 +25,7 @@ import docking.widgets.fieldpanel.listener.LayoutModelListener;
|
||||||
* using a BigFieldPanel.
|
* 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
|
* Returns true if every index returns a non-null layout and all the layouts
|
||||||
|
@ -68,6 +68,7 @@ public interface LayoutModel {
|
||||||
*
|
*
|
||||||
* @return new iterator
|
* @return new iterator
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public default LayoutModelIterator iterator() {
|
public default LayoutModelIterator iterator() {
|
||||||
return new LayoutModelIterator(this);
|
return new LayoutModelIterator(this);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue