diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index 60a83be33c5f088f62e01f732e88fc2570077287..59a35f71ee0067d11e2e748aea20b22d255fb044 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -130,6 +130,11 @@ public final class JavaClass { return null; } + public Integer getSourceLine(int decompiledLine) { + decompile(); + return cls.getCode().getLineMapping().get(decompiledLine); + } + public String getFullName() { return cls.getFullName(); } diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java index 1e7f528a272afa285a041b68252cda88d1bb8e27..7b43305359ee3be017e5a763dc49a3b4969350d4 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -63,12 +63,10 @@ public class ClassGen { addClassCode(clsBody); CodeWriter clsCode = new CodeWriter(); - if (!"".equals(cls.getPackage())) { clsCode.add("package ").add(cls.getPackage()).add(';'); clsCode.newLine(); } - int importsCount = imports.size(); if (importsCount != 0) { List sortImports = new ArrayList(importsCount); @@ -85,7 +83,6 @@ public class ClassGen { sortImports.clear(); imports.clear(); } - clsCode.add(clsBody); return clsCode; } diff --git a/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java b/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java index 7c6b894964eb400954a05a2a9ee936cfdd4ad656..34d2b13ee09e2ff62d50cddfd41d616e3a62404f 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java +++ b/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java @@ -37,17 +37,13 @@ public class CodeWriter { private int line = 1; private int offset = 0; private Map annotations = Collections.emptyMap(); + private Map lineMap = Collections.emptyMap(); public CodeWriter() { this.indent = 0; this.indentStr = ""; } - public CodeWriter(int indent) { - this.indent = indent; - updateIndent(); - } - public CodeWriter startLine() { addLine(); addLineIndent(); @@ -68,11 +64,6 @@ public class CodeWriter { return this; } - public CodeWriter add(Object obj) { - add(obj.toString()); - return this; - } - public CodeWriter add(String str) { buf.append(str); offset += str.length(); @@ -91,6 +82,9 @@ public class CodeWriter { CodePosition pos = entry.getKey(); attachAnnotation(entry.getValue(), new CodePosition(line + pos.getLine(), pos.getOffset())); } + for (Map.Entry entry : code.lineMap.entrySet()) { + attachSourceLine(line + entry.getKey(), entry.getValue()); + } line += code.line; offset = code.offset; buf.append(code); @@ -102,6 +96,11 @@ public class CodeWriter { return this; } + public CodeWriter addIndent() { + add(INDENT); + return this; + } + private void addLine() { buf.append(NL); line++; @@ -114,11 +113,6 @@ public class CodeWriter { return this; } - public CodeWriter addIndent() { - add(INDENT); - return this; - } - private void updateIndent() { int curIndent = indent; if (curIndent < INDENT_CACHE.length) { @@ -178,17 +172,32 @@ public class CodeWriter { return attachAnnotation(obj, new CodePosition(line, offset + 1)); } - private Object attachAnnotation(Object obj, CodePosition pos) { - if (annotations.isEmpty()) { - annotations = new HashMap(); + private void attachSourceLine(int decompiledLine, int sourceLine) { + if (lineMap.isEmpty()) { + lineMap = new HashMap(); } - return annotations.put(pos, obj); + lineMap.put(decompiledLine, sourceLine); } public Map getAnnotations() { return annotations; } + public void attachSourceLine(int sourceLine) { + attachSourceLine(line, sourceLine); + } + + public Map getLineMapping() { + return lineMap; + } + + private Object attachAnnotation(Object obj, CodePosition pos) { + if (annotations.isEmpty()) { + annotations = new HashMap(); + } + return annotations.put(pos, obj); + } + public void finish() { buf.trimToSize(); Iterator> it = annotations.entrySet().iterator(); @@ -206,9 +215,8 @@ public class CodeWriter { private static String removeFirstEmptyLine(String str) { if (str.startsWith(NL)) { return str.substring(NL.length()); - } else { - return str; } + return str; } public int length() { diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 2c51b2c50ecd3cc1d7dadc27ad960419a57552b7..ed2510c88b9cac19322dd527453ac4393405e569 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -198,7 +198,7 @@ public class InsnGen { } else { code.startLine(); if (insn.getSourceLine() != 0) { - code.attachAnnotation(insn.getSourceLine()); + code.attachSourceLine(insn.getSourceLine()); } if (insn.getResult() != null && insn.getType() != InsnType.ARITH_ONEARG) { assignVar(code, insn); @@ -469,7 +469,8 @@ public class InsnGen { code.add(") {"); code.incIndent(); for (int i = 0; i < sw.getCasesCount(); i++) { - code.startLine("case ").add(sw.getKeys()[i]).add(": goto "); + String key = sw.getKeys()[i].toString(); + code.startLine("case ").add(key).add(": goto "); code.add(MethodGen.getLabelName(sw.getTargets()[i])).add(';'); } code.startLine("default: goto "); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java index 8ae179c729599f434659dce319a292a54d2630fa..0671f6227865605366433b682ab4ef97aa18445b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java @@ -120,7 +120,7 @@ public class DotGraphVisitor extends AbstractVisitor { if (region instanceof IRegion) { IRegion r = (IRegion) region; dot.startLine("subgraph " + makeName(region) + " {"); - dot.startLine("label = \"").add(r); + dot.startLine("label = \"").add(r.toString()); String attrs = attributesString(r); if (attrs.length() != 0) { dot.add(" | ").add(attrs); @@ -146,7 +146,7 @@ public class DotGraphVisitor extends AbstractVisitor { dot.add("color=red,"); } dot.add("label=\"{"); - dot.add(block.getId()).add("\\:\\ "); + dot.add(String.valueOf(block.getId())).add("\\:\\ "); dot.add(InsnUtils.formatOffset(block.getStartOffset())); if (attrs.length() != 0) { dot.add('|').add(attrs); @@ -210,7 +210,7 @@ public class DotGraphVisitor extends AbstractVisitor { } return str.toString(); } else { - CodeWriter code = new CodeWriter(0); + CodeWriter code = new CodeWriter(); MethodGen.addFallbackInsns(code, mth, block.getInstructions(), false); String str = escape(code.newLine().toString()); if (str.startsWith(NL)) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java index ab0b7ca46726918b2b7e637442ad78e4eb0357c2..a90ca6e2744de0c3c7987d281a42e5c7b56234ba 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java @@ -30,7 +30,7 @@ class CodeArea extends RSyntaxTextArea { private static final long serialVersionUID = 6312736869579635796L; - private static final Color BACKGROUND = new Color(0xf7f7f7); + public static final Color BACKGROUND = new Color(0xf7f7f7); private static final Color JUMP_FOREGROUND = new Color(0x785523); private static final Color JUMP_BACKGROUND = new Color(0xE6E6FF); @@ -96,7 +96,11 @@ class CodeArea extends RSyntaxTextArea { } Position getCurrentPosition() { - return new Position(cls, getCaretLineNumber()); + return new Position(cls, getCaretLineNumber() + 1); + } + + Integer getSourceLine(int line) { + return cls.getCls().getSourceLine(line); } void scrollToLine(int line) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java index ef206714aa2a656bf1d6c59ef134e9db0a13b1e3..fe6de5fca57361a50d1702f91f23b4dff558b4fd 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java @@ -5,14 +5,13 @@ import jadx.gui.utils.Utils; import javax.swing.AbstractAction; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.KeyStroke; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; -import org.fife.ui.rtextarea.RTextScrollPane; - class CodePanel extends JPanel { private static final long serialVersionUID = 5310536092010045565L; @@ -21,7 +20,7 @@ class CodePanel extends JPanel { private final JClass jClass; private final SearchBar searchBar; private final CodeArea codeArea; - private final RTextScrollPane scrollPane; + private final JScrollPane scrollPane; CodePanel(TabbedPane panel, JClass cls) { tabbedPane = panel; @@ -29,8 +28,8 @@ class CodePanel extends JPanel { codeArea = new CodeArea(this); searchBar = new SearchBar(codeArea); - scrollPane = new RTextScrollPane(codeArea); - scrollPane.setFoldIndicatorEnabled(true); + scrollPane = new JScrollPane(codeArea); + scrollPane.setRowHeaderView(new LineNumbers(codeArea)); setLayout(new BorderLayout()); add(searchBar, BorderLayout.NORTH); @@ -65,7 +64,7 @@ class CodePanel extends JPanel { return codeArea; } - RTextScrollPane getScrollPane() { + JScrollPane getScrollPane() { return scrollPane; } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/LineNumbers.java new file mode 100644 index 0000000000000000000000000000000000000000..f253b4f4efa93abd1e8544f31721af6b00b3f072 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/LineNumbers.java @@ -0,0 +1,188 @@ +package jadx.gui.ui; + +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.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; +import javax.swing.text.StyleConstants; +import javax.swing.text.Utilities; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.HashMap; + +public class LineNumbers extends JPanel implements CaretListener { + + private final static Border OUTER = new MatteBorder(0, 0, 0, 1, Color.LIGHT_GRAY); + private final static int HEIGHT = Integer.MAX_VALUE - 1000000; + + public static final Color FOREGROUND = Color.GRAY; + public static final Color BACKGROUND = CodeArea.BACKGROUND; + public static final Color CURRENT_LINE_FOREGROUND = new Color(227, 0, 0); + + private CodeArea codeArea; + private boolean useSourceLines = true; + + private int lastDigits; + private int lastLine; + private HashMap fonts; + + public LineNumbers(CodeArea component) { + this.codeArea = component; + setFont(component.getFont()); + setBackground(BACKGROUND); + setForeground(FOREGROUND); + + setBorderGap(5); + setPreferredWidth(); + + component.addCaretListener(this); + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + useSourceLines = !useSourceLines; + repaint(); + } + } + }); + } + + public void setBorderGap(int borderGap) { + Border inner = new EmptyBorder(0, borderGap, 0, borderGap); + setBorder(new CompoundBorder(OUTER, inner)); + lastDigits = 0; + } + + private void setPreferredWidth() { + Element root = codeArea.getDocument().getDefaultRootElement(); + int lines = root.getElementCount(); + int digits = Math.max(String.valueOf(lines).length(), 3); + 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, HEIGHT); + setPreferredSize(d); + setSize(d); + } + } + } + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + FontMetrics fontMetrics = codeArea.getFontMetrics(codeArea.getFont()); + Insets insets = getInsets(); + int availableWidth = getSize().width - insets.left - insets.right; + Rectangle clip = g.getClipBounds(); + int rowStartOffset = codeArea.viewToModel(new Point(0, clip.y)); + int endOffset = codeArea.viewToModel(new Point(0, clip.y + clip.height)); + + while (rowStartOffset <= endOffset) { + try { + if (isCurrentLine(rowStartOffset)) { + g.setColor(CURRENT_LINE_FOREGROUND); + } else { + g.setColor(FOREGROUND); + } + String lineNumber = getTextLineNumber(rowStartOffset); + int stringWidth = fontMetrics.stringWidth(lineNumber); + int x = availableWidth - stringWidth + insets.left; + int y = getOffsetY(rowStartOffset, fontMetrics); + g.drawString(lineNumber, x, y); + rowStartOffset = Utilities.getRowEnd(codeArea, rowStartOffset) + 1; + } catch (Exception e) { + break; + } + } + } + + private boolean isCurrentLine(int rowStartOffset) { + int caretPosition = codeArea.getCaretPosition(); + Element root = codeArea.getDocument().getDefaultRootElement(); + return root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition); + } + + protected String getTextLineNumber(int rowStartOffset) { + Element root = codeArea.getDocument().getDefaultRootElement(); + int index = root.getElementIndex(rowStartOffset); + Element line = root.getElement(index); + if (line.getStartOffset() == rowStartOffset) { + int lineNumber = index + 1; + if (useSourceLines) { + Integer sourceLine = codeArea.getSourceLine(lineNumber); + if (sourceLine != null) { + return String.valueOf(sourceLine); + } + } else { + return String.valueOf(lineNumber); + } + } + return ""; + } + + private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics) throws BadLocationException { + Rectangle r = codeArea.modelToView(rowStartOffset); + if (r == null) { + throw new BadLocationException("Can't get Y offset", rowStartOffset); + } + int lineHeight = fontMetrics.getHeight(); + int y = r.y + r.height; + int descent = 0; + if (r.height == lineHeight) { + descent = fontMetrics.getDescent(); + } else { + if (fonts == null) { + fonts = new HashMap(); + } + Element root = codeArea.getDocument().getDefaultRootElement(); + int index = root.getElementIndex(rowStartOffset); + Element line = root.getElement(index); + for (int i = 0; i < line.getElementCount(); i++) { + Element child = line.getElement(i); + AttributeSet as = child.getAttributes(); + String fontFamily = (String) as.getAttribute(StyleConstants.FontFamily); + Integer fontSize = (Integer) as.getAttribute(StyleConstants.FontSize); + String key = fontFamily + fontSize; + FontMetrics fm = fonts.get(key); + if (fm == null) { + Font font = new Font(fontFamily, Font.PLAIN, fontSize); + fm = codeArea.getFontMetrics(font); + fonts.put(key, fm); + } + descent = Math.max(descent, fm.getDescent()); + } + } + return y - descent; + } + + @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; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 6ce5d6ab2212c417ebcc575b00a7580e08239060..74cf11e164eb813db8abbf6fae228c3a4ce8c9be 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -54,13 +54,13 @@ public class MainWindow extends JFrame { private static final double BORDER_RATIO = 0.15; private static final double WINDOW_RATIO = 1 - BORDER_RATIO * 2; + private static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15; private static final ImageIcon ICON_OPEN = Utils.openIcon("folder"); private static final ImageIcon ICON_SAVE_ALL = Utils.openIcon("disk_multiple"); private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross"); private static final ImageIcon ICON_FLAT_PKG = Utils.openIcon("empty_logical_package_obj"); private static final ImageIcon ICON_SEARCH = Utils.openIcon("magnifier"); - private static final ImageIcon ICON_BACK = Utils.openIcon("icon_back"); private static final ImageIcon ICON_FORWARD = Utils.openIcon("icon_forward"); @@ -248,7 +248,7 @@ public class MainWindow extends JFrame { private void initUI() { mainPanel = new JPanel(new BorderLayout()); JSplitPane splitPane = new JSplitPane(); - splitPane.setResizeWeight(0.2); + splitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); mainPanel.add(splitPane); DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("Please open file"); diff --git a/jadx-gui/src/test/groovy/jadx/gui/tests/TestJumpManager.groovy b/jadx-gui/src/test/groovy/jadx/gui/tests/TestJumpManager.groovy index 0f46e53a1149525486fe182f7f1109c43b3fd504..9339f1c05156e500af6b2492970931cdc8575220 100644 --- a/jadx-gui/src/test/groovy/jadx/gui/tests/TestJumpManager.groovy +++ b/jadx-gui/src/test/groovy/jadx/gui/tests/TestJumpManager.groovy @@ -36,6 +36,7 @@ class TestJumpManager extends Specification { then: noExceptionThrown() jm.getPrev() == mock1 + jm.getPrev() == null jm.getNext() == mock2 jm.getNext() == null }