Commit 3bc96719 authored by Skylot's avatar Skylot

perf(gui): speed up line numbers rendering (#714)

parent 7fd959e6
package jadx.gui.ui.codearea; package jadx.gui.ui.codearea;
import java.awt.BorderLayout; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import javax.swing.AbstractAction; import javax.swing.*;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import org.fife.ui.rtextarea.RTextScrollPane;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JResource;
import jadx.gui.ui.ContentPanel; import jadx.gui.ui.ContentPanel;
import jadx.gui.utils.UiUtils; import jadx.gui.utils.UiUtils;
...@@ -21,21 +21,21 @@ public class CodePanel extends JPanel { ...@@ -21,21 +21,21 @@ public class CodePanel extends JPanel {
private final SearchBar searchBar; private final SearchBar searchBar;
private final AbstractCodeArea codeArea; private final AbstractCodeArea codeArea;
private final JScrollPane codeScrollPane; private final RTextScrollPane codeScrollPane;
@SuppressWarnings("serial")
public CodePanel(ContentPanel contentPanel, AbstractCodeArea codeArea) { public CodePanel(ContentPanel contentPanel, AbstractCodeArea codeArea) {
this.codeArea = codeArea; this.codeArea = codeArea;
searchBar = new SearchBar(codeArea); searchBar = new SearchBar(codeArea);
codeScrollPane = new JScrollPane(codeArea); codeScrollPane = new RTextScrollPane(codeArea, false);
setLayout(new BorderLayout()); setLayout(new BorderLayout());
add(searchBar, BorderLayout.NORTH); add(searchBar, BorderLayout.NORTH);
add(codeScrollPane, BorderLayout.CENTER); add(codeScrollPane, BorderLayout.CENTER);
initLineNumbers();
KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton()); KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton());
UiUtils.addKeyBinding(codeArea, key, "SearchAction", new AbstractAction() { UiUtils.addKeyBinding(codeArea, key, "SearchAction", new AbstractAction() {
private static final long serialVersionUID = 71338030532869694L;
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
searchBar.toggle(); searchBar.toggle();
...@@ -50,20 +50,19 @@ public class CodePanel extends JPanel { ...@@ -50,20 +50,19 @@ public class CodePanel extends JPanel {
public void load() { public void load() {
codeArea.load(); codeArea.load();
initLineNumbers();
} }
private void initLineNumbers() { private void initLineNumbers() {
// TODO: fix slow line rendering on big files
if (codeArea.getDocument().getLength() <= 100_000) {
LineNumbers numbers = new LineNumbers(codeArea); LineNumbers numbers = new LineNumbers(codeArea);
numbers.setUseSourceLines(isUseSourceLines()); numbers.setUseSourceLines(isUseSourceLines());
codeScrollPane.setRowHeaderView(numbers); codeScrollPane.setRowHeaderView(numbers);
} }
}
private boolean isUseSourceLines() { private boolean isUseSourceLines() {
if (codeArea.getNode() instanceof JResource) { JNode node = codeArea.getNode();
JResource resNode = (JResource) codeArea.getNode(); if (node instanceof JResource) {
JResource resNode = (JResource) node;
return !resNode.getLineMapping().isEmpty(); return !resNode.getLineMapping().isEmpty();
} }
return false; return false;
......
...@@ -3,7 +3,6 @@ package jadx.gui.ui.codearea; ...@@ -3,7 +3,6 @@ package jadx.gui.ui.codearea;
import java.awt.*; import java.awt.*;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.swing.*; import javax.swing.*;
...@@ -13,11 +12,7 @@ import javax.swing.border.EmptyBorder; ...@@ -13,11 +12,7 @@ import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder; import javax.swing.border.MatteBorder;
import javax.swing.event.CaretEvent; import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener; import javax.swing.event.CaretListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element; import javax.swing.text.Element;
import javax.swing.text.StyleConstants;
import javax.swing.text.Utilities;
import org.fife.ui.rsyntaxtextarea.SyntaxScheme; import org.fife.ui.rsyntaxtextarea.SyntaxScheme;
import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.Token;
...@@ -36,19 +31,23 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -36,19 +31,23 @@ public class LineNumbers extends JPanel implements CaretListener {
private final AbstractCodeArea codeArea; private final AbstractCodeArea codeArea;
private boolean useSourceLines = true; private boolean useSourceLines = true;
private int lastDigits; private transient int lastDigits;
private int lastLine; private transient int lastLine;
private Map<String, FontMetrics> fonts;
private final transient Color numberColor; private final transient Color numberColor;
private final transient Color normalNumColor;
private final transient Color currentColor; private final transient Color currentColor;
private final transient Border border; private final transient Border border;
private transient Insets textAreaInsets;
private transient Rectangle visibleRect = new Rectangle();
public LineNumbers(AbstractCodeArea codeArea) { public LineNumbers(AbstractCodeArea codeArea) {
this.codeArea = codeArea; this.codeArea = codeArea;
setFont(codeArea.getFont()); setFont(codeArea.getFont());
SyntaxScheme syntaxScheme = codeArea.getSyntaxScheme(); SyntaxScheme syntaxScheme = codeArea.getSyntaxScheme();
numberColor = syntaxScheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground; numberColor = syntaxScheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground;
normalNumColor = syntaxScheme.getStyle(Token.ANNOTATION).foreground;
currentColor = syntaxScheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground; currentColor = syntaxScheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground;
border = new MatteBorder(0, 0, 0, 1, syntaxScheme.getStyle(Token.COMMENT_MULTILINE).foreground); border = new MatteBorder(0, 0, 0, 1, syntaxScheme.getStyle(Token.COMMENT_MULTILINE).foreground);
setBackground(codeArea.getBackground()); setBackground(codeArea.getBackground());
...@@ -78,7 +77,7 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -78,7 +77,7 @@ public class LineNumbers extends JPanel implements CaretListener {
private void setPreferredWidth() { private void setPreferredWidth() {
Element root = codeArea.getDocument().getDefaultRootElement(); Element root = codeArea.getDocument().getDefaultRootElement();
int lines = root.getElementCount(); int lines = root.getElementCount();
int digits = Math.max(String.valueOf(lines).length(), 3); int digits = Math.max(String.valueOf(lines).length(), 4);
if (lastDigits != digits) { if (lastDigits != digits) {
lastDigits = digits; lastDigits = digits;
FontMetrics fontMetrics = getFontMetrics(getFont()); FontMetrics fontMetrics = getFontMetrics(getFont());
...@@ -97,7 +96,13 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -97,7 +96,13 @@ public class LineNumbers extends JPanel implements CaretListener {
@Override @Override
public void paintComponent(Graphics g) { public void paintComponent(Graphics g) {
super.paintComponent(g); visibleRect = g.getClipBounds(visibleRect);
if (visibleRect == null) {
visibleRect = getVisibleRect();
}
if (visibleRect == null) {
return;
}
applyRenderHints(g); applyRenderHints(g);
Font font = codeArea.getFont(); Font font = codeArea.getFont();
...@@ -110,26 +115,39 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -110,26 +115,39 @@ public class LineNumbers extends JPanel implements CaretListener {
FontMetrics fontMetrics = codeArea.getFontMetrics(font); FontMetrics fontMetrics = codeArea.getFontMetrics(font);
Insets insets = getInsets(); Insets insets = getInsets();
int availableWidth = size.width - insets.left - insets.right; int availableWidth = size.width - 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) { int cellHeight = codeArea.getLineHeight();
try { int ascent = codeArea.getMaxAscent();
String lineNumber = getTextLineNumber(rowStartOffset);
if (lineNumber != null) { textAreaInsets = codeArea.getInsets(textAreaInsets);
if (isCurrentLine(rowStartOffset)) { if (visibleRect.y < textAreaInsets.top) {
g.setColor(currentColor); visibleRect.height -= (textAreaInsets.top - visibleRect.y);
} else { visibleRect.y = textAreaInsets.top;
g.setColor(numberColor);
} }
int stringWidth = fontMetrics.stringWidth(lineNumber);
int x = availableWidth - stringWidth + insets.left; int topLine = (visibleRect.y - textAreaInsets.top) / cellHeight;
int y = getOffsetY(rowStartOffset, fontMetrics); int actualTopY = topLine * cellHeight + textAreaInsets.top;
g.drawString(lineNumber, x, y); int y = actualTopY + ascent;
int endY = visibleRect.y + visibleRect.height + ascent;
Element rootElement = codeArea.getDocument().getDefaultRootElement();
int currentLine = 1 + rootElement.getElementIndex(codeArea.getCaretPosition());
int lineNum = topLine + 1;
int linesCount = codeArea.getLineCount();
boolean isCurLine = updateColor(g, false, true);
while (y < endY && lineNum <= linesCount) {
try {
String lineStr = getTextLineNumber(lineNum);
if (lineStr != null) {
isCurLine = updateColor(g, lineNum == currentLine, isCurLine);
int x = availableWidth - fontMetrics.stringWidth(lineStr);
g.drawString(lineStr, x, y);
} else if (!useSourceLines) {
break;
} }
rowStartOffset = Utilities.getRowEnd(codeArea, rowStartOffset) + 1; lineNum++;
y += cellHeight;
} catch (Exception e) { } catch (Exception e) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Line numbers draw error", e); LOG.debug("Line numbers draw error", e);
...@@ -139,6 +157,17 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -139,6 +157,17 @@ public class LineNumbers extends JPanel implements CaretListener {
} }
} }
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) { private void applyRenderHints(Graphics g) {
if (g instanceof Graphics2D) { if (g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g; Graphics2D g2d = (Graphics2D) g;
...@@ -152,63 +181,17 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -152,63 +181,17 @@ public class LineNumbers extends JPanel implements CaretListener {
} }
} }
private boolean isCurrentLine(int rowStartOffset) {
int caretPosition = codeArea.getCaretPosition();
Element root = codeArea.getDocument().getDefaultRootElement();
return root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition);
}
@Nullable @Nullable
protected String getTextLineNumber(int rowStartOffset) { protected String getTextLineNumber(int lineNumber) {
Element root = codeArea.getDocument().getDefaultRootElement(); if (!useSourceLines) {
int index = root.getElementIndex(rowStartOffset); return String.valueOf(lineNumber);
Element line = root.getElement(index);
if (line.getStartOffset() != rowStartOffset) {
return null;
} }
int lineNumber = index + 1;
if (useSourceLines) {
Integer sourceLine = codeArea.getSourceLine(lineNumber); Integer sourceLine = codeArea.getSourceLine(lineNumber);
if (sourceLine == null) { if (sourceLine == null) {
return null; return null;
} }
return String.valueOf(sourceLine); return String.valueOf(sourceLine);
} }
return String.valueOf(lineNumber);
}
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.computeIfAbsent(key, k -> {
Font font = new Font(fontFamily, Font.PLAIN, fontSize);
return codeArea.getFontMetrics(font);
});
descent = Math.max(descent, fm.getDescent());
}
}
return y - descent;
}
@Override @Override
public void caretUpdate(CaretEvent e) { public void caretUpdate(CaretEvent e) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment