From 5c0c1daa71ad4c8b3bfefbbedbd77b44e4f06060 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 16 Feb 2024 17:43:42 +0000 Subject: [PATCH] fix(gui): use new RSTA line number formatter API to show source lines --- .../gui/ui/codearea/AbstractCodeArea.java | 4 +- .../java/jadx/gui/ui/codearea/CodeArea.java | 1 + .../java/jadx/gui/ui/codearea/CodePanel.java | 74 +++-- .../jadx/gui/ui/codearea/LineNumbers.java | 287 ------------------ .../gui/ui/codearea/SourceLineFormatter.java | 39 +++ 5 files changed, 86 insertions(+), 319 deletions(-) delete mode 100644 jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/codearea/SourceLineFormatter.java diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 8da27240d01..7cda350331c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -36,7 +36,6 @@ 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 AbstractCodeArea(ContentPanel contentPanel, JNode node) { private void applyEditableProperties(JNode node) { boolean editable = node.isEditable(); setEditable(editable); - setCodeFoldingEnabled(editable); if (editable) { setCloseCurlyBraces(true); setCloseMarkupTags(true); @@ -337,7 +335,7 @@ public static boolean isWordToken(@Nullable Token token) { } } - public abstract @NotNull ICodeInfo getCodeInfo(); + public abstract ICodeInfo getCodeInfo(); /** * Implement in this method the code that loads and sets the content to be displayed diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index a58e00b0e83..da0328d0d95 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -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); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index dc558cb5fa6..78c035ff48c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -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.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.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 void load() { initLineNumbers(); } - private JScrollPane buildCodeScrollPane(AbstractCodeArea codeArea) { - if (codeArea instanceof SmaliArea) { - return new RTextScrollPane(codeArea); + private synchronized void initLineNumbers() { + codeScrollPane.getGutter().setLineNumberFont(getSettings().getFont()); + LineNumbersMode mode = getLineNumbersMode(); + if (mode == LineNumbersMode.DISABLE) { + codeScrollPane.setLineNumbersEnabled(false); + return; } - return new JScrollPane(codeArea); + useSourceLines = mode == LineNumbersMode.DEBUG; + applyLineFormatter(); + codeScrollPane.setLineNumbersEnabled(true); } - private void initLineNumbers() { - if (codeArea instanceof SmaliArea) { - return; - } + 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 @@ private void initLineNumbers() { // 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 @@ private boolean canShowDebugLines() { 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; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java deleted file mode 100644 index 55e2de57f68..00000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java +++ /dev/null @@ -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; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SourceLineFormatter.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SourceLineFormatter.java new file mode 100644 index 00000000000..adcf1c0afda --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SourceLineFormatter.java @@ -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(); + } +}