fix(gui): use new RSTA line number formatter API to show source lines

This commit is contained in:
Skylot
2024-02-16 17:43:42 +00:00
parent 603ea3989a
commit 5c0c1daa71
5 changed files with 87 additions and 320 deletions
@@ -36,7 +36,6 @@ import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
import org.fife.ui.rsyntaxtextarea.TokenTypes;
import org.fife.ui.rtextarea.SearchContext;
import org.fife.ui.rtextarea.SearchEngine;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -108,7 +107,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
private void applyEditableProperties(JNode node) {
boolean editable = node.isEditable();
setEditable(editable);
setCodeFoldingEnabled(editable);
if (editable) {
setCloseCurlyBraces(true);
setCloseMarkupTags(true);
@@ -337,7 +335,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
}
public abstract @NotNull ICodeInfo getCodeInfo();
public abstract ICodeInfo getCodeInfo();
/**
* Implement in this method the code that loads and sets the content to be displayed
@@ -61,6 +61,7 @@ public final class CodeArea extends AbstractCodeArea {
}
setHyperlinksEnabled(true);
setCodeFoldingEnabled(true);
setLinkScanningMask(InputEvent.CTRL_DOWN_MASK);
CodeLinkGenerator codeLinkGenerator = new CodeLinkGenerator(this);
setLinkGenerator(codeLinkGenerator);
@@ -1,6 +1,7 @@
package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
@@ -21,10 +22,9 @@ import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.PopupMenuEvent;
import org.fife.ui.rtextarea.Gutter;
import org.fife.ui.rtextarea.LineNumberFormatter;
import org.fife.ui.rtextarea.LineNumberList;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.core.utils.StringUtils;
@@ -36,28 +36,32 @@ import jadx.gui.utils.CaretPositionFix;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.MousePressedHandler;
/**
* A panel combining a {@link SearchBar and a scollable {@link CodeArea}
*/
public class CodePanel extends JPanel {
private static final Logger LOG = LoggerFactory.getLogger(CodePanel.class);
private static final long serialVersionUID = 1117721869391885865L;
private final SearchBar searchBar;
private final AbstractCodeArea codeArea;
private final JScrollPane codeScrollPane;
private final RTextScrollPane codeScrollPane;
private boolean useSourceLines;
public CodePanel(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
this.searchBar = new SearchBar(codeArea);
this.codeScrollPane = buildCodeScrollPane(codeArea);
this.codeScrollPane = new RTextScrollPane(codeArea);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
add(searchBar, BorderLayout.NORTH);
add(codeScrollPane, BorderLayout.CENTER);
initLinesModeSwitch();
KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton());
UiUtils.addKeyBinding(codeArea, key, "SearchAction", new AbstractAction() {
private static final long serialVersionUID = 71338030532869694L;
@@ -118,17 +122,26 @@ public class CodePanel extends JPanel {
initLineNumbers();
}
private JScrollPane buildCodeScrollPane(AbstractCodeArea codeArea) {
if (codeArea instanceof SmaliArea) {
return new RTextScrollPane(codeArea);
}
return new JScrollPane(codeArea);
}
private void initLineNumbers() {
if (codeArea instanceof SmaliArea) {
private synchronized void initLineNumbers() {
codeScrollPane.getGutter().setLineNumberFont(getSettings().getFont());
LineNumbersMode mode = getLineNumbersMode();
if (mode == LineNumbersMode.DISABLE) {
codeScrollPane.setLineNumbersEnabled(false);
return;
}
useSourceLines = mode == LineNumbersMode.DEBUG;
applyLineFormatter();
codeScrollPane.setLineNumbersEnabled(true);
}
private synchronized void applyLineFormatter() {
LineNumberFormatter linesFormatter = useSourceLines
? new SourceLineFormatter(codeArea.getCodeInfo())
: LineNumberList.DEFAULT_LINE_NUMBER_FORMATTER;
codeScrollPane.getGutter().setLineNumberFormatter(linesFormatter);
}
private LineNumbersMode getLineNumbersMode() {
LineNumbersMode mode = getSettings().getLineNumbersMode();
boolean canShowDebugLines = canShowDebugLines();
if (mode == LineNumbersMode.AUTO) {
@@ -137,24 +150,13 @@ public class CodePanel extends JPanel {
// nothing to show => hide lines view
mode = LineNumbersMode.DISABLE;
}
switch (mode) {
case DISABLE:
codeScrollPane.setRowHeaderView(null);
break;
case NORMAL:
Gutter gutter = new Gutter(codeArea);
gutter.setLineNumberFont(getSettings().getFont());
codeScrollPane.setRowHeaderView(gutter);
break;
case DEBUG:
LineNumbers jadxGutter = new LineNumbers(codeArea);
jadxGutter.setUseSourceLines(true);
codeScrollPane.setRowHeaderView(jadxGutter);
break;
}
return mode;
}
private boolean canShowDebugLines() {
if (codeArea instanceof SmaliArea) {
return false;
}
ICodeInfo codeInfo = codeArea.getCodeInfo();
if (!codeInfo.hasMetadata()) {
return false;
@@ -167,6 +169,20 @@ public class CodePanel extends JPanel {
return uniqueDebugLines.size() > 3;
}
private void initLinesModeSwitch() {
if (canShowDebugLines()) {
MousePressedHandler lineModeSwitch = new MousePressedHandler(ev -> {
useSourceLines = !useSourceLines;
applyLineFormatter();
});
for (Component gutterComp : codeScrollPane.getGutter().getComponents()) {
if (gutterComp instanceof LineNumberList) {
gutterComp.addMouseListener(lineModeSwitch);
}
}
}
}
public SearchBar getSearchBar() {
return searchBar;
}
@@ -1,287 +0,0 @@
package jadx.gui.ui.codearea;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Map;
import javax.swing.JPanel;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.Element;
import javax.swing.text.View;
import org.fife.ui.rsyntaxtextarea.SyntaxScheme;
import org.fife.ui.rsyntaxtextarea.Token;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
public class LineNumbers extends JPanel implements CaretListener {
private static final Logger LOG = LoggerFactory.getLogger(LineNumbers.class);
private static final long serialVersionUID = -4978268673635308190L;
private static final int NUM_HEIGHT = Integer.MAX_VALUE - 1000000;
private static final Map<?, ?> DESKTOP_HINTS = (Map<?, ?>) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints");
private final transient AbstractCodeArea codeArea;
private final transient ICodeInfo codeInfo;
private boolean useSourceLines = true;
private transient int lastDigits;
private transient int lastLine;
private final transient Color numberColor;
private final transient Color normalNumColor;
private final transient Color currentColor;
private final transient Border border;
private transient Insets textAreaInsets;
private transient Rectangle visibleRect = new Rectangle();
public LineNumbers(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
this.codeInfo = codeArea.getCodeInfo();
setFont(codeArea.getFont());
SyntaxScheme syntaxScheme = codeArea.getSyntaxScheme();
numberColor = syntaxScheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground;
normalNumColor = syntaxScheme.getStyle(Token.ANNOTATION).foreground;
currentColor = syntaxScheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground;
border = new MatteBorder(0, 0, 0, 1, syntaxScheme.getStyle(Token.COMMENT_MULTILINE).foreground);
setBackground(codeArea.getBackground());
setForeground(numberColor);
setBorderGap(5);
setPreferredWidth();
codeArea.addCaretListener(this);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
useSourceLines = !useSourceLines;
repaint();
}
});
}
public void setBorderGap(int borderGap) {
Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
setBorder(new CompoundBorder(border, inner));
lastDigits = 0;
}
private void setPreferredWidth() {
Element root = codeArea.getDocument().getDefaultRootElement();
int lines = root.getElementCount();
int digits = Math.max(numberLength(lines), numberLength(getMaxDebugLine()));
if (lastDigits != digits) {
lastDigits = digits;
FontMetrics fontMetrics = getFontMetrics(getFont());
int width = fontMetrics.charWidth('0') * digits;
Insets insets = getInsets();
int preferredWidth = insets.left + insets.right + width;
Dimension d = getPreferredSize();
if (d != null) {
d.setSize(preferredWidth, NUM_HEIGHT);
setPreferredSize(d);
setSize(d);
}
}
}
private int numberLength(int value) {
return String.valueOf(value).length();
}
@SuppressWarnings("deprecation")
@Override
public void paintComponent(Graphics g) {
visibleRect = g.getClipBounds(visibleRect);
if (visibleRect == null) {
visibleRect = getVisibleRect();
}
if (visibleRect == null) {
return;
}
applyRenderHints(g);
Font baseFont = codeArea.getFont();
Font font = baseFont.deriveFont(baseFont.getSize2D() - 1.0f);
g.setFont(font);
Dimension size = getSize();
g.setColor(codeArea.getBackground());
g.fillRect(0, visibleRect.y, size.width, visibleRect.height);
FontMetrics fontMetrics = codeArea.getFontMetrics(font);
Insets insets = getInsets();
int availableWidth = size.width - insets.right;
textAreaInsets = codeArea.getInsets(textAreaInsets);
if (visibleRect.y < textAreaInsets.top) {
visibleRect.height -= (textAreaInsets.top - visibleRect.y);
visibleRect.y = textAreaInsets.top;
}
boolean lineWrap = codeArea.getLineWrap();
int cellHeight = codeArea.getLineHeight();
int ascent = codeArea.getMaxAscent();
int currentLine = codeArea.getCaretLineNumber();
int y;
int topLine;
int linesCount;
View parentView = null;
Rectangle editorRect = null;
if (lineWrap) {
Element root = codeArea.getDocument().getDefaultRootElement();
parentView = codeArea.getUI().getRootView(codeArea).getView(0);
int topPosition = codeArea.viewToModel(new Point(visibleRect.x, visibleRect.y));
topLine = root.getElementIndex(topPosition);
linesCount = root.getElementCount();
editorRect = getEditorBoundingRect();
Rectangle topLineBounds = getLineBounds(parentView, topLine, editorRect);
if (topLineBounds == null) {
return;
}
y = ascent + topLineBounds.y;
} else {
linesCount = codeArea.getLineCount();
topLine = (visibleRect.y - textAreaInsets.top) / cellHeight;
y = ascent + topLine * cellHeight + textAreaInsets.top;
}
int endY = visibleRect.y + visibleRect.height + ascent;
int lineNum = topLine;
boolean isCurLine = updateColor(g, false, true);
while (y < endY && lineNum < linesCount) {
try {
String lineStr = getTextLineNumber(lineNum + 1);
if (lineStr != null) {
isCurLine = updateColor(g, lineNum == currentLine, isCurLine);
int x = availableWidth - fontMetrics.stringWidth(lineStr);
g.drawString(lineStr, x, y);
}
if (lineWrap) {
Rectangle lineBounds = getLineBounds(parentView, lineNum, editorRect);
if (lineBounds == null) {
return;
}
y += lineBounds.height;
} else {
y += cellHeight;
}
lineNum++;
} catch (Exception e) {
LOG.debug("Line numbers draw error", e);
break;
}
}
}
private Rectangle getLineBounds(View parent, int line, Rectangle editorRect) {
Shape alloc = parent.getChildAllocation(line, editorRect);
if (alloc == null) {
return null;
}
if (alloc instanceof Rectangle) {
return (Rectangle) alloc;
}
return alloc.getBounds();
}
protected Rectangle getEditorBoundingRect() {
Rectangle bounds = codeArea.getBounds();
if (bounds.width <= 0 || bounds.height <= 0) {
return null;
}
bounds.x = 0;
bounds.y = 0;
Insets insets = codeArea.getInsets();
bounds.x += insets.left;
bounds.y += insets.top;
bounds.width -= insets.left + insets.right;
bounds.height -= insets.top + insets.bottom;
return bounds;
}
private boolean updateColor(Graphics g, boolean newCurLine, boolean oldCurLine) {
if (oldCurLine != newCurLine) {
if (newCurLine) {
g.setColor(currentColor);
} else {
g.setColor(useSourceLines ? numberColor : normalNumColor);
}
}
return newCurLine;
}
private void applyRenderHints(Graphics g) {
if (g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g;
if (DESKTOP_HINTS != null) {
g2d.setRenderingHints(DESKTOP_HINTS);
} else {
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
}
}
}
@Nullable
protected String getTextLineNumber(int lineNumber) {
if (!useSourceLines) {
return String.valueOf(lineNumber);
}
Integer sourceLine = codeInfo.getCodeMetadata().getLineMapping().get(lineNumber);
if (sourceLine == null) {
return null;
}
return String.valueOf(sourceLine);
}
private int getMaxDebugLine() {
return codeInfo.getCodeMetadata().getLineMapping()
.keySet().stream()
.mapToInt(Integer::intValue)
.max().orElse(0);
}
@Override
public void caretUpdate(CaretEvent e) {
int caretPosition = codeArea.getCaretPosition();
Element root = codeArea.getDocument().getDefaultRootElement();
int currentLine = root.getElementIndex(caretPosition);
if (lastLine != currentLine) {
repaint();
lastLine = currentLine;
}
}
public boolean isUseSourceLines() {
return useSourceLines;
}
public void setUseSourceLines(boolean useSourceLines) {
this.useSourceLines = useSourceLines;
}
}
@@ -0,0 +1,39 @@
package jadx.gui.ui.codearea;
import org.fife.ui.rtextarea.LineNumberFormatter;
import jadx.api.ICodeInfo;
public class SourceLineFormatter implements LineNumberFormatter {
private final ICodeInfo codeInfo;
private final int maxLength;
public SourceLineFormatter(ICodeInfo codeInfo) {
this.codeInfo = codeInfo;
this.maxLength = calcMaxLength(codeInfo);
}
@Override
public String format(int lineNumber) {
Integer sourceLine = codeInfo.getCodeMetadata().getLineMapping().get(lineNumber);
if (sourceLine == null) {
return "";
}
return String.valueOf(sourceLine);
}
@Override
public int getMaxLength(int maxLineNumber) {
return maxLength;
}
private static int calcMaxLength(ICodeInfo codeInfo) {
int maxLine = codeInfo.getCodeMetadata().getLineMapping()
.values().stream()
.mapToInt(Integer::intValue)
.max().orElse(1);
// maxLine can be anything including zero and negative numbers,
// so use safe 'stringify' method instead faster 'Math.log10'
return Integer.toString(maxLine).length();
}
}