feat(gui): add code comments (#359) (PR #1127)

* feat(gui): add code comments (#359)
* refactor: replace instanceof search with method dispatch in RegionGen
* fix: various bug fixes and improvements for code comments
* fix(gui): support multiline code comments
* fix: resolve code differences after class reload
* fix(gui): add search for comments, allow search in active tab only
* fix: correct search for inner classes
* fix(gui): run full index on search dialog open
This commit is contained in:
skylot
2021-03-04 17:45:48 +03:00
committed by GitHub
parent 7a14aaa17e
commit 4e5fac4b88
113 changed files with 3527 additions and 722 deletions
@@ -20,6 +20,7 @@ import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaPackage;
import jadx.api.ResourceFile;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import static jadx.gui.utils.FileUtils.toFiles;
@@ -29,6 +30,7 @@ public class JadxWrapper {
private final JadxSettings settings;
private JadxDecompiler decompiler;
private JadxProject project;
private List<Path> openPaths = Collections.emptyList();
public JadxWrapper(JadxSettings settings) {
@@ -41,6 +43,7 @@ public class JadxWrapper {
try {
JadxArgs jadxArgs = settings.toJadxArgs();
jadxArgs.setInputFiles(toFiles(paths));
jadxArgs.setCodeData(project.getCodeData());
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
@@ -155,10 +158,14 @@ public class JadxWrapper {
return decompiler.getArgs();
}
public void setProject(JadxProject project) {
this.project = project;
}
/**
* @param fullName Full name of an outer class. Inner classes are not supported.
*/
public @Nullable JavaClass searchJavaClassByClassName(String fullName) {
public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) {
return decompiler.getClasses().stream()
.filter(cls -> cls.getFullName().equals(fullName))
.findFirst()
@@ -166,10 +173,7 @@ public class JadxWrapper {
}
public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) {
return decompiler.getClasses().stream()
.filter(cls -> cls.getClassNode().getClassInfo().getFullName().equals(fullName))
.findFirst()
.orElse(null);
return decompiler.searchJavaClassByOrigFullName(fullName);
}
/**
@@ -20,6 +20,7 @@ import jadx.gui.utils.search.TextSearchIndex;
public class IndexJob extends BackgroundJob {
private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class);
private final CacheObject cache;
public IndexJob(JadxWrapper wrapper, CacheObject cache, int threadsCount) {
@@ -29,18 +30,14 @@ public class IndexJob extends BackgroundJob {
@Override
protected void runJob() {
TextSearchIndex index = new TextSearchIndex(cache);
CodeUsageInfo usageInfo = new CodeUsageInfo(cache.getNodeCache());
cache.setTextIndex(index);
cache.setUsageInfo(usageInfo);
TextSearchIndex index = cache.getTextIndex();
addTask(index::indexResource);
for (final JavaClass cls : wrapper.getIncludedClasses()) {
addTask(() -> indexCls(cache, cls));
}
}
public static void indexCls(CacheObject cache, JavaClass cls) {
private static void indexCls(CacheObject cache, JavaClass cls) {
try {
TextSearchIndex index = cache.getTextIndex();
CodeUsageInfo usageInfo = cache.getUsageInfo();
@@ -1,11 +1,12 @@
package jadx.gui.settings;
import java.io.BufferedWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
@@ -14,63 +15,76 @@ import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.api.data.ICodeComment;
import jadx.api.data.IJavaNodeRef;
import jadx.api.data.impl.JadxCodeComment;
import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxNodeRef;
import jadx.core.utils.GsonUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.PathTypeAdapter;
public class JadxProject {
private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class);
private static final int CURRENT_SETTINGS_VERSION = 0;
private static final int CURRENT_PROJECT_VERSION = 1;
public static final String PROJECT_EXTENSION = "jadx";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
.registerTypeAdapter(ICodeComment.class, GsonUtils.interfaceReplace(JadxCodeComment.class))
.registerTypeAdapter(IJavaNodeRef.class, GsonUtils.interfaceReplace(JadxNodeRef.class))
.setPrettyPrinting()
.create();
private transient MainWindow mainWindow;
private transient JadxSettings settings;
private transient String name = "New Project";
private transient Path projectPath;
private List<Path> filesPath;
private List<String[]> treeExpansions = new ArrayList<>();
private transient boolean saved;
private transient boolean initial = true;
private transient boolean saved;
private int projectVersion = 0;
private List<Path> files;
private List<String[]> treeExpansions = new ArrayList<>();
private JadxCodeData codeData = new JadxCodeData();
private int projectVersion;
// Don't remove. Used in json serialization
public JadxProject() {
}
public JadxProject(JadxSettings settings) {
this.settings = settings;
}
public void setSettings(JadxSettings settings) {
this.settings = settings;
}
public void setMainWindow(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
public Path getProjectPath() {
return projectPath;
}
private void setProjectPath(Path projectPath) {
this.projectPath = projectPath;
if (projectVersion != CURRENT_SETTINGS_VERSION) {
upgradeSettings(projectVersion);
}
name = projectPath.getFileName().toString();
name = name.substring(0, name.lastIndexOf('.'));
int dotPos = name.lastIndexOf('.');
if (dotPos != -1) {
name = name.substring(0, dotPos);
}
changed();
}
public List<Path> getFilePaths() {
return filesPath;
return files;
}
public void setFilePath(List<Path> files) {
if (!files.equals(getFilePaths())) {
this.filesPath = files;
this.files = files;
changed();
}
}
@@ -85,11 +99,7 @@ public class JadxProject {
}
public void removeTreeExpansion(String[] expansion) {
for (Iterator<String[]> it = treeExpansions.iterator(); it.hasNext();) {
if (isParentOfExpansion(expansion, it.next())) {
it.remove();
}
}
treeExpansions.removeIf(strings -> isParentOfExpansion(expansion, strings));
changed();
}
@@ -106,13 +116,25 @@ public class JadxProject {
return false;
}
public JadxCodeData getCodeData() {
return codeData;
}
public void setCodeData(JadxCodeData codeData) {
this.codeData = codeData;
changed();
}
private void changed() {
if (settings.isAutoSaveProject()) {
if (settings != null && settings.isAutoSaveProject()) {
save();
} else {
saved = false;
}
initial = false;
if (mainWindow != null) {
mainWindow.updateProject(this);
}
}
public String getName() {
@@ -134,8 +156,8 @@ public class JadxProject {
public void save() {
if (getProjectPath() != null) {
try (BufferedWriter writer = Files.newBufferedWriter(getProjectPath())) {
writer.write(GSON.toJson(this));
try (Writer writer = Files.newBufferedWriter(getProjectPath(), StandardCharsets.UTF_8)) {
GSON.toJson(this, writer);
saved = true;
} catch (Exception e) {
LOG.error("Error saving project", e);
@@ -143,29 +165,29 @@ public class JadxProject {
}
}
public static JadxProject from(Path path, JadxSettings settings) {
try {
List<String> lines = Files.readAllLines(path);
if (!lines.isEmpty()) {
JadxProject project = GSON.fromJson(lines.get(0), JadxProject.class);
project.settings = settings;
project.setProjectPath(path);
project.saved = true;
return project;
}
public static JadxProject from(Path path) {
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
JadxProject project = GSON.fromJson(reader, JadxProject.class);
project.saved = true;
project.setProjectPath(path);
project.upgrade();
return project;
} catch (Exception e) {
LOG.error("Error loading project", e);
return null;
}
return null;
}
private void upgradeSettings(int fromVersion) {
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
private void upgrade() {
int fromVersion = projectVersion;
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_PROJECT_VERSION);
if (fromVersion == 0) {
fromVersion++;
}
projectVersion = CURRENT_SETTINGS_VERSION;
if (fromVersion != CURRENT_PROJECT_VERSION) {
throw new JadxRuntimeException("Project update failed");
}
projectVersion = CURRENT_PROJECT_VERSION;
save();
}
}
@@ -166,7 +166,10 @@ public class JadxSettings extends JadxCLIArgs {
return Collections.unmodifiableList(recentProjects);
}
public void addRecentProject(Path projectPath) {
public void addRecentProject(@Nullable Path projectPath) {
if (projectPath == null) {
return;
}
recentProjects.remove(projectPath);
recentProjects.add(0, projectPath);
int count = recentProjects.size();
@@ -64,9 +64,6 @@ public class JadxSettingsAdapter {
} else {
settings.fixOnLoad();
}
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded settings: {}", makeString(settings));
}
return settings;
} catch (Exception e) {
LOG.error("Error load settings. Settings will reset.\n Loaded json string: {}", jsonSettings, e);
@@ -77,7 +74,6 @@ public class JadxSettingsAdapter {
public static void store(JadxSettings settings) {
try {
String jsonSettings = makeString(settings);
LOG.debug("Saving settings: {}", jsonSettings);
PREFS.put(JADX_GUI_KEY, jsonSettings);
PREFS.sync();
} catch (Exception e) {
@@ -1,6 +1,6 @@
package jadx.gui.treemodel;
import javax.swing.*;
import javax.swing.Icon;
import jadx.api.JavaNode;
import jadx.gui.utils.search.StringRef;
@@ -13,14 +13,14 @@ public class CodeNode extends JNode {
private final transient JClass jParent;
private final transient StringRef line;
private final transient int lineNum;
private transient int pos = -1;
private transient boolean precise;
private transient int pos;
public CodeNode(JNode jNode, int lineNum, StringRef lineStr) {
public CodeNode(JNode jNode, StringRef lineStr, int lineNum, int pos) {
this.jNode = jNode;
this.jParent = this.jNode.getJParent();
this.line = lineStr;
this.lineNum = lineNum;
this.pos = pos;
}
@Override
@@ -79,6 +79,11 @@ public class CodeNode extends JNode {
return makeString();
}
@Override
public String getSyntaxName() {
return jNode.getSyntaxName();
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -96,24 +101,8 @@ public class CodeNode extends JNode {
return jNode.hashCode();
}
@Override
public int getPos() {
return pos;
}
public CodeNode setPos(int pos) {
this.pos = pos;
return this;
}
public CodeNode setPrecisePos(int pos) {
this.pos = pos;
if (pos > -1) {
this.precise = true;
}
return this;
}
public boolean isPrecisePos() {
return precise;
}
}
@@ -3,6 +3,8 @@ package jadx.gui.treemodel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import jadx.api.JavaField;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
@@ -71,6 +73,11 @@ public class JField extends JNode {
return icon;
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_JAVA;
}
@Override
public String makeString() {
return UiUtils.typeFormat(field.getName(), field.getType());
@@ -5,6 +5,8 @@ import java.util.Iterator;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
@@ -73,6 +75,11 @@ public class JMethod extends JNode {
return icon;
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_JAVA;
}
@Override
public boolean canRename() {
return !mth.getMethodNode().contains(AFlag.DONT_RENAME);
@@ -101,6 +101,14 @@ public abstract class JNode extends DefaultMutableTreeNode {
return makeLongString();
}
public int getPos() {
JavaNode javaNode = getJavaNode();
if (javaNode == null) {
return -1;
}
return javaNode.getDefPos();
}
@Override
public String toString() {
return makeString();
@@ -6,6 +6,8 @@ import jadx.api.JavaNode;
import jadx.api.JavaVariable;
public class JVariable extends JNode {
private static final long serialVersionUID = -3002100457834453783L;
JClass cls;
JavaVariable var;
@@ -0,0 +1,247 @@
package jadx.gui.ui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.data.ICodeComment;
import jadx.api.data.impl.JadxCodeComment;
import jadx.api.data.impl.JadxCodeData;
import jadx.gui.settings.JadxProject;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
public class CommentDialog extends JDialog {
private static final long serialVersionUID = -1865682124935757528L;
private static final Logger LOG = LoggerFactory.getLogger(CommentDialog.class);
public static void show(CodeArea codeArea, ICodeComment blankComment) {
ICodeComment existComment = searchForExistComment(codeArea, blankComment);
Dialog dialog;
if (existComment != null) {
dialog = new CommentDialog(codeArea, existComment, true);
} else {
dialog = new CommentDialog(codeArea, blankComment, false);
}
dialog.setVisible(true);
}
private static void updateCommentsData(CodeArea codeArea, Consumer<List<ICodeComment>> updater) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
if (codeData == null) {
codeData = new JadxCodeData();
}
List<ICodeComment> list = new ArrayList<>(codeData.getComments());
updater.accept(list);
Collections.sort(list);
codeData.setComments(list);
project.setCodeData(codeData);
} catch (Exception e) {
LOG.error("Comment action failed", e);
}
try {
// refresh code
codeArea.refreshClass();
} catch (Exception e) {
LOG.error("Failed to reload code", e);
}
}
private static ICodeComment searchForExistComment(CodeArea codeArea, ICodeComment blankComment) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
if (codeData == null || codeData.getComments().isEmpty()) {
return null;
}
for (ICodeComment comment : codeData.getComments()) {
if (Objects.equals(comment.getNodeRef(), blankComment.getNodeRef())
&& comment.getOffset() == blankComment.getOffset()
&& comment.getAttachType() == blankComment.getAttachType()) {
return comment;
}
}
} catch (Exception e) {
LOG.error("Error searching for exists comment", e);
}
return null;
}
private final transient CodeArea codeArea;
private final transient ICodeComment comment;
private final transient boolean updateComment;
private transient JTextArea commentArea;
public CommentDialog(CodeArea codeArea, ICodeComment comment, boolean updateComment) {
super(codeArea.getMainWindow());
this.codeArea = codeArea;
this.comment = comment;
this.updateComment = updateComment;
initUI();
}
private void apply() {
String newCommentStr = commentArea.getText().trim();
if (newCommentStr.isEmpty()) {
if (updateComment) {
remove();
} else {
cancel();
}
return;
}
ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(),
newCommentStr, comment.getOffset(), comment.getAttachType());
if (updateComment) {
updateCommentsData(codeArea, list -> {
list.remove(comment);
list.add(newComment);
});
} else {
updateCommentsData(codeArea, list -> list.add(newComment));
}
dispose();
}
private void remove() {
updateCommentsData(codeArea, list -> list.removeIf(c -> c == comment));
dispose();
}
private void cancel() {
dispose();
}
private void initUI() {
commentArea = new JTextArea();
TextStandardActions.attach(commentArea);
commentArea.setEditable(true);
commentArea.setFont(codeArea.getMainWindow().getSettings().getFont());
commentArea.setAlignmentX(Component.LEFT_ALIGNMENT);
commentArea.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER:
if (e.isShiftDown() || e.isControlDown()) {
commentArea.append("\n");
} else {
apply();
}
break;
case KeyEvent.VK_ESCAPE:
cancel();
break;
}
}
});
if (updateComment) {
commentArea.setText(comment.getComment());
}
JScrollPane textAreaScrollPane = new JScrollPane(commentArea);
textAreaScrollPane.setAlignmentX(LEFT_ALIGNMENT);
JLabel commentLabel = new JLabel(NLS.str("comment_dialog.label"), SwingConstants.LEFT);
JLabel usageLabel = new JLabel(NLS.str("comment_dialog.usage"), SwingConstants.LEFT);
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
mainPanel.add(commentLabel);
mainPanel.add(Box.createRigidArea(new Dimension(0, 5)));
mainPanel.add(textAreaScrollPane);
mainPanel.add(Box.createRigidArea(new Dimension(0, 5)));
mainPanel.add(usageLabel);
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JPanel buttonPane = initButtonsPanel();
Container contentPane = getContentPane();
contentPane.add(mainPanel, BorderLayout.CENTER);
contentPane.add(buttonPane, BorderLayout.PAGE_END);
if (updateComment) {
setTitle(NLS.str("comment_dialog.title.update"));
} else {
setTitle(NLS.str("comment_dialog.title.add"));
}
pack();
if (!codeArea.getMainWindow().getSettings().loadWindowPos(this)) {
setSize(800, 140);
}
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.APPLICATION_MODAL);
UiUtils.addEscapeShortCutToDispose(this);
}
protected JPanel initButtonsPanel() {
JButton cancelButton = new JButton(NLS.str("common_dialog.cancel"));
cancelButton.addActionListener(event -> cancel());
String applyStr = updateComment ? NLS.str("common_dialog.update") : NLS.str("common_dialog.add");
JButton renameBtn = new JButton(applyStr);
renameBtn.addActionListener(event -> apply());
getRootPane().setDefaultButton(renameBtn);
JButton removeBtn;
if (updateComment) {
removeBtn = new JButton(NLS.str("common_dialog.remove"));
removeBtn.addActionListener(event -> remove());
} else {
removeBtn = null;
}
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
buttonPane.add(Box.createRigidArea(new Dimension(5, 0)));
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(renameBtn);
if (removeBtn != null) {
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(removeBtn);
}
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelButton);
return buttonPane;
}
@Override
public void dispose() {
codeArea.getMainWindow().getSettings().saveWindowPos(this);
super.dispose();
}
}
@@ -1,6 +1,11 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
@@ -13,14 +18,27 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.SearchContext;
import org.fife.ui.rtextarea.SearchEngine;
import org.jetbrains.annotations.NotNull;
@@ -31,7 +49,8 @@ import org.slf4j.LoggerFactory;
import jadx.gui.jobs.BackgroundJob;
import jadx.gui.jobs.BackgroundWorker;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.treemodel.*;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JumpPosition;
@@ -112,16 +131,9 @@ public abstract class CommonSearchDialog extends JDialog {
JumpPosition jmpPos;
JNode node = (JNode) resultsModel.getValueAt(selectedId, 0);
if (node instanceof JResSearchNode) {
jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getLine())
.setPrecise(((JResSearchNode) node).getPos());
} else if (node instanceof CodeNode) {
CodeNode codeNode = (CodeNode) node;
jmpPos = new JumpPosition(node.getRootClass(), node.getLine(), codeNode.getPos());
if (codeNode.isPrecisePos()) {
jmpPos.setPrecise(codeNode.getPos());
}
jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getLine(), node.getPos());
} else {
jmpPos = new JumpPosition(node.getRootClass(), node.getLine());
jmpPos = new JumpPosition(node.getRootClass(), node.getLine(), node.getPos());
}
tabbedPane.codeJump(jmpPos);
if (!mainWindow.getSettings().getKeepCommonDialogOpen()) {
@@ -285,11 +297,11 @@ public abstract class CommonSearchDialog extends JDialog {
int firstColMaxWidth = (int) (width * 0.5);
int rowCount = getRowCount();
int columnCount = getColumnCount();
if (!model.isAddDescColumn()) {
boolean addDescColumn = model.isAddDescColumn();
if (!addDescColumn) {
firstColMaxWidth = width;
}
Component nodeComp = null;
Component codeComp = null;
setRowHeight(10); // reset all rows height
for (int col = 0; col < columnCount; col++) {
int colWidth = 50;
for (int row = 0; row < rowCount; row++) {
@@ -298,11 +310,9 @@ public abstract class CommonSearchDialog extends JDialog {
continue;
}
colWidth = Math.max(comp.getPreferredSize().width, colWidth);
if (nodeComp == null && col == 0) {
nodeComp = comp;
}
if (codeComp == null && col == 1) {
codeComp = comp;
int h = Math.max(getRowHeight(row), getHeight(comp));
if (h > 1) {
setRowHeight(row, h);
}
}
colWidth += 10;
@@ -314,10 +324,7 @@ public abstract class CommonSearchDialog extends JDialog {
TableColumn column = columnModel.getColumn(col);
column.setPreferredWidth(colWidth);
}
// setRowHeight(Math.max(nodeComp.getPreferredSize().height, codeComp.getPreferredSize().height +
// 4));
updateUI();
setRowHeight(Math.max(getHeight(nodeComp), getHeight(codeComp) + 4));
}
private int getHeight(@Nullable Component nodeComp) {
@@ -487,20 +494,25 @@ public abstract class CommonSearchDialog extends JDialog {
if (!node.hasDescString()) {
return emptyLabel;
}
RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(mainWindow);
textArea.setLayout(new GridLayout(1, 1));
textArea.setEditable(false);
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
textArea.setText(" " + node.makeDescString());
textArea.setRows(1);
textArea.setColumns(textArea.getText().length() + 1);
textArea.setSyntaxEditingStyle(node.getSyntaxName());
String descStr = node.makeDescString();
textArea.setText(descStr);
if (descStr.contains("\n")) {
textArea.setRows(textArea.getLineCount());
} else {
textArea.setRows(1);
textArea.setColumns(descStr.length() + 1);
}
if (highlightText != null) {
SearchContext searchContext = new SearchContext(highlightText);
searchContext.setMatchCase(!highlightTextCaseInsensitive);
searchContext.setMarkAll(true);
searchContext.setRegularExpression(highlightTextUseRegex);
searchContext.setMarkAll(true);
SearchEngine.markAll(textArea, searchContext);
}
textArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
return textArea;
}
@@ -106,12 +106,15 @@ import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.CodeUsageInfo;
import jadx.gui.utils.FontUtils;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.search.CommentsIndex;
import jadx.gui.utils.search.TextSearchIndex;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
import static jadx.gui.utils.FileUtils.fileNamesToPaths;
@@ -137,6 +140,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_FLAT_PKG = UiUtils.openIcon("empty_logical_package_obj");
private static final ImageIcon ICON_SEARCH = UiUtils.openIcon("wand");
private static final ImageIcon ICON_FIND = UiUtils.openIcon("magnifier");
private static final ImageIcon ICON_COMMENT_SEARCH = UiUtils.openIcon("table_edit");
private static final ImageIcon ICON_BACK = UiUtils.openIcon("icon_back");
private static final ImageIcon ICON_FORWARD = UiUtils.openIcon("icon_forward");
private static final ImageIcon ICON_PREF = UiUtils.openIcon("wrench");
@@ -219,7 +223,7 @@ public class MainWindow extends JFrame {
private void handleSelectClassOption() {
if (settings.getCmdSelectClass() != null) {
JavaNode javaNode = wrapper.searchJavaClassByClassName(settings.getCmdSelectClass());
JavaNode javaNode = wrapper.searchJavaClassByFullAlias(settings.getCmdSelectClass());
if (javaNode == null) {
javaNode = wrapper.searchJavaClassByOrigClassName(settings.getCmdSelectClass());
}
@@ -230,8 +234,7 @@ public class MainWindow extends JFrame {
return;
}
JNode node = cacheObject.getNodeCache().makeFrom(javaNode);
tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine())
.setPrecise(JumpPosition.getDefPos(node)));
tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine(), JumpPosition.getDefPos(node)));
}
}
@@ -311,9 +314,10 @@ public class MainWindow extends JFrame {
if (!ensureProjectIsSaved()) {
return;
}
project = new JadxProject(settings);
update();
cancelBackgroundJobs();
clearTree();
wrapper.close();
updateProject(new JadxProject());
}
private void saveProject() {
@@ -356,6 +360,7 @@ public class MainWindow extends JFrame {
}
}
project.saveAs(path);
settings.addRecentProject(path);
update();
}
}
@@ -408,18 +413,18 @@ public class MainWindow extends JFrame {
if (!ensureProjectIsSaved()) {
return;
}
project = JadxProject.from(path, settings);
if (project == null) {
JadxProject jadxProject = JadxProject.from(path);
if (jadxProject == null) {
JOptionPane.showMessageDialog(
this,
NLS.str("msg.project_error"),
NLS.str("msg.project_error_title"),
JOptionPane.INFORMATION_MESSAGE);
return;
jadxProject = new JadxProject();
}
update();
updateProject(jadxProject);
settings.addRecentProject(path);
List<Path> filePaths = project.getFilePaths();
List<Path> filePaths = jadxProject.getFilePaths();
if (filePaths == null) {
clearTree();
} else {
@@ -427,6 +432,15 @@ public class MainWindow extends JFrame {
}
}
public void updateProject(JadxProject jadxProject) {
jadxProject.setSettings(settings);
jadxProject.setMainWindow(this);
this.project = jadxProject;
this.wrapper.setProject(jadxProject);
this.cacheObject.setCommentsIndex(new CommentsIndex(wrapper, cacheObject, jadxProject));
update();
}
private void update() {
newProjectAction.setEnabled(!project.isInitial());
saveProjectAction.setEnabled(!project.isSaved());
@@ -444,17 +458,14 @@ public class MainWindow extends JFrame {
protected void resetCache() {
cacheObject.reset();
// TODO: decompilation freezes sometime with several threads
this.cacheObject.setJRoot(treeRoot);
this.cacheObject.setJadxSettings(settings);
cacheObject.setJRoot(treeRoot);
cacheObject.setJadxSettings(settings);
int threadsCount = settings.getThreadsCount();
cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount));
cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount));
}
public void resetIndex() {
int threadsCount = settings.getThreadsCount();
cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount));
cacheObject.setUsageInfo(new CodeUsageInfo(cacheObject.getNodeCache()));
cacheObject.setTextIndex(new TextSearchIndex(this));
}
synchronized void runBackgroundJobs() {
@@ -471,7 +482,9 @@ public class MainWindow extends JFrame {
}
public synchronized void cancelBackgroundJobs() {
backgroundExecutor.cancelAll();
if (backgroundExecutor != null) {
backgroundExecutor.cancelAll();
}
if (backgroundWorker != null) {
backgroundWorker.stop();
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
@@ -518,8 +531,7 @@ public class MainWindow extends JFrame {
continue;
}
JNode newNode = cacheObject.getNodeCache().makeFrom(newClass);
tabbedPane.codeJump(new JumpPosition(newNode, position)
.setPrecise(JumpPosition.getDefPos(newNode)));
tabbedPane.codeJump(new JumpPosition(newNode, position, JumpPosition.getDefPos(newNode)));
}
}
@@ -651,12 +663,7 @@ public class MainWindow extends JFrame {
} else if (obj instanceof ApkSignature) {
tabbedPane.showSimpleNode((JNode) obj);
} else if (obj instanceof JNode) {
JNode node = (JNode) obj;
JClass cls = node.getRootClass();
if (cls != null) {
tabbedPane.codeJump(new JumpPosition(cls, node.getLine())
.setPrecise(JumpPosition.getDefPos(node)));
}
tabbedPane.codeJump(new JumpPosition((JNode) obj));
}
} catch (Exception e) {
LOG.error("Content loading error", e);
@@ -829,7 +836,7 @@ public class MainWindow extends JFrame {
return;
}
}
new SearchDialog(MainWindow.this, true).setVisible(true);
SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT);
}
};
textSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.text_search"));
@@ -839,12 +846,22 @@ public class MainWindow extends JFrame {
Action clsSearchAction = new AbstractAction(NLS.str("menu.class_search"), ICON_FIND) {
@Override
public void actionPerformed(ActionEvent e) {
new SearchDialog(MainWindow.this, false).setVisible(true);
SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS);
}
};
clsSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.class_search"));
clsSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_N, UiUtils.ctrlButton()));
Action commentSearchAction = new AbstractAction(NLS.str("menu.comment_search"), ICON_COMMENT_SEARCH) {
@Override
public void actionPerformed(ActionEvent e) {
SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.COMMENT);
}
};
commentSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.comment_search"));
commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON,
UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));
Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -925,6 +942,7 @@ public class MainWindow extends JFrame {
nav.setMnemonic(KeyEvent.VK_N);
nav.add(textSearchAction);
nav.add(clsSearchAction);
nav.add(commentSearchAction);
nav.addSeparator();
nav.add(backAction);
nav.add(forwardAction);
@@ -969,6 +987,7 @@ public class MainWindow extends JFrame {
toolbar.addSeparator();
toolbar.add(textSearchAction);
toolbar.add(clsSearchAction);
toolbar.add(commentSearchAction);
toolbar.addSeparator();
toolbar.add(backAction);
toolbar.add(forwardAction);
@@ -1202,6 +1221,10 @@ public class MainWindow extends JFrame {
return wrapper;
}
public JadxProject getProject() {
return project;
}
public TabbedPane getTabbedPane() {
return tabbedPane;
}
@@ -4,7 +4,11 @@ import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
@@ -23,7 +27,11 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.*;
import jadx.api.ICodeWriter;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.deobf.DeobfPresets;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
@@ -35,10 +43,19 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.*;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JPackage;
import jadx.gui.treemodel.JVariable;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.codearea.CodePanel;
import jadx.gui.utils.*;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
public class RenameDialog extends JDialog {
private static final long serialVersionUID = -3269715644416902410L;
@@ -235,17 +252,11 @@ public class RenameDialog extends JDialog {
private void refreshTabs(TabbedPane tabbedPane, Set<JClass> updatedClasses) {
for (Map.Entry<JNode, ContentPanel> entry : tabbedPane.getOpenTabs().entrySet()) {
ContentPanel contentPanel = entry.getValue();
if (contentPanel instanceof ClassCodeContentPanel) {
JNode node = entry.getKey();
JClass rootClass = node.getRootClass();
if (updatedClasses.contains(rootClass)) {
refreshJClass(rootClass);
ClassCodeContentPanel codePanel = (ClassCodeContentPanel) contentPanel;
CodePanel javaPanel = codePanel.getJavaCodePanel();
javaPanel.refresh();
tabbedPane.refresh(rootClass);
}
JClass rootClass = entry.getKey().getRootClass();
if (updatedClasses.remove(rootClass)) {
ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) entry.getValue();
CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea();
codeArea.refreshClass();
}
}
}
@@ -254,7 +265,7 @@ public class RenameDialog extends JDialog {
protected JPanel initButtonsPanel() {
JButton cancelButton = new JButton(NLS.str("search_dialog.cancel"));
cancelButton.addActionListener(event -> dispose());
JButton renameBtn = new JButton(NLS.str("popup.rename"));
JButton renameBtn = new JButton(NLS.str("common_dialog.ok"));
renameBtn.addActionListener(event -> rename());
getRootPane().setDefaultButton(renameBtn);
@@ -1,13 +1,26 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.swing.*;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
@@ -21,109 +34,195 @@ import io.reactivex.Flowable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import jadx.core.utils.StringUtils;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.search.TextSearchIndex;
public class SearchDialog extends CommonSearchDialog {
private static final long serialVersionUID = -5105405456969134105L;
private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class);
private static final long serialVersionUID = -5105405456969134105L;
private final boolean textSearch;
public static void search(MainWindow window, SearchPreset preset) {
SearchDialog searchDialog = new SearchDialog(window, preset, Collections.emptySet());
searchDialog.setVisible(true);
}
public static void searchInActiveTab(MainWindow window, SearchPreset preset) {
SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(SearchOptions.ACTIVE_TAB));
searchDialog.setVisible(true);
}
public static void searchText(MainWindow window, String text) {
SearchDialog searchDialog = new SearchDialog(window, SearchPreset.TEXT, Collections.emptySet());
searchDialog.initSearchText = text;
searchDialog.setVisible(true);
}
public enum SearchPreset {
TEXT, CLASS, COMMENT
}
public enum SearchOptions {
CLASS,
METHOD,
FIELD,
CODE,
RESOURCE,
COMMENT,
IGNORE_CASE,
USE_REGEX,
Resource
ACTIVE_TAB
}
private transient Set<SearchOptions> options;
private final transient SearchPreset searchPreset;
private final transient Set<SearchOptions> options;
private transient JTextField searchField;
private transient Disposable searchDisposable;
private transient SearchEventEmitter searchEmitter;
private transient String text = null;
private transient ChangeListener activeTabListener;
public SearchDialog(MainWindow mainWindow, boolean textSearch) {
private transient String initSearchText = null;
private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set<SearchOptions> additionalOptions) {
super(mainWindow);
this.textSearch = textSearch;
if (textSearch) {
Set<SearchOptions> lastSearchOptions = cache.getLastSearchOptions();
if (!lastSearchOptions.isEmpty()) {
this.options = lastSearchOptions;
} else {
this.options = EnumSet.of(SearchOptions.CODE, SearchOptions.IGNORE_CASE);
}
} else {
this.options = EnumSet.of(SearchOptions.CLASS);
}
this.searchPreset = preset;
this.options = buildOptions(preset);
this.options.addAll(additionalOptions);
initUI();
searchFieldSubscribe();
registerInitOnOpen();
loadWindowPos();
registerActiveTabListener();
}
@Override
public void dispose() {
if (searchDisposable != null && !searchDisposable.isDisposed()) {
searchDisposable.dispose();
}
removeActiveTabListener();
super.dispose();
}
private Set<SearchOptions> buildOptions(SearchPreset preset) {
Set<SearchOptions> searchOptions = cache.getLastSearchOptions().get(preset);
if (searchOptions == null) {
searchOptions = new HashSet<>();
}
switch (preset) {
case TEXT:
if (searchOptions.isEmpty()) {
searchOptions.add(SearchOptions.CODE);
searchOptions.add(SearchOptions.IGNORE_CASE);
}
break;
case CLASS:
searchOptions.add(SearchOptions.CLASS);
break;
case COMMENT:
searchOptions.add(SearchOptions.COMMENT);
searchOptions.remove(SearchOptions.ACTIVE_TAB);
break;
}
return searchOptions;
}
@Override
protected void openInit() {
prepare();
String lastSearch = cache.getLastSearch();
if (lastSearch != null) {
searchField.setText(lastSearch);
String searchText = initSearchText != null ? initSearchText : cache.getLastSearch();
if (searchText != null) {
searchField.setText(searchText);
searchField.selectAll();
}
searchField.requestFocus();
if (searchField.getText().isEmpty()) {
checkIndex();
}
searchEmitter.emitSearch();
}
private TextSearchIndex checkIndex() {
if (!cache.getIndexJob().isComplete()) {
if (isFullIndexNeeded()) {
prepare();
}
}
return cache.getTextIndex();
}
private boolean isFullIndexNeeded() {
for (SearchOptions option : options) {
switch (option) {
case CLASS:
case METHOD:
case FIELD:
// TODO: split indexes so full decompilation not needed for these
return true;
case CODE:
return true;
case RESOURCE:
case COMMENT:
// full index not needed
break;
}
}
return false;
}
private void initUI() {
JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
searchField = new JTextField();
searchField.setAlignmentX(LEFT_ALIGNMENT);
new TextStandardActions(searchField);
searchFieldSubscribe();
TextStandardActions.attach(searchField);
JCheckBox caseChBox = makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE);
JCheckBox regexChBox = makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX);
JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
findLabel.setAlignmentX(LEFT_ALIGNMENT);
JCheckBox resChBox = makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.Resource);
JCheckBox clsChBox = makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS);
JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD);
JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD);
JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE);
JPanel searchFieldPanel = new JPanel();
searchFieldPanel.setLayout(new BoxLayout(searchFieldPanel, BoxLayout.PAGE_AXIS));
searchFieldPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
searchFieldPanel.setAlignmentX(LEFT_ALIGNMENT);
searchFieldPanel.add(findLabel);
searchFieldPanel.add(Box.createRigidArea(new Dimension(0, 5)));
searchFieldPanel.add(searchField);
JPanel searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in")));
searchInPanel.add(resChBox);
searchInPanel.add(clsChBox);
searchInPanel.add(mthChBox);
searchInPanel.add(fldChBox);
searchInPanel.add(codeChBox);
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS));
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD));
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD));
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE));
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.RESOURCE));
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.comments"), SearchOptions.COMMENT));
JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.options")));
searchOptions.add(caseChBox);
searchOptions.add(regexChBox);
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.active_tab"), SearchOptions.ACTIVE_TAB));
Box box = Box.createHorizontalBox();
box.setAlignmentX(LEFT_ALIGNMENT);
box.add(searchInPanel);
box.add(searchOptions);
JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT));
optionsPanel.setAlignmentX(LEFT_ALIGNMENT);
optionsPanel.add(searchInPanel);
optionsPanel.add(searchOptions);
JPanel searchPane = new JPanel();
searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS));
findLabel.setLabelFor(searchField);
searchPane.add(findLabel);
searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
searchPane.add(searchField);
searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
searchPane.add(box);
searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
searchPane.add(searchFieldPanel);
searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
searchPane.add(optionsPanel);
initCommon();
JPanel resultsPanel = initResultsTable();
@@ -181,9 +280,7 @@ public class SearchDialog extends CommonSearchDialog {
Flowable<String> textChanges = onTextFieldChanges(searchField);
Flowable<String> searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable());
searchDisposable = searchEvents
.filter(text -> text.length() > 0)
.subscribeOn(Schedulers.single())
.doOnNext(r -> LOG.debug("search event: {}", r))
.switchMap(text -> prepareSearch(text)
.doOnError(e -> LOG.error("Error prepare search: {}", e.getMessage(), e))
.subscribeOn(Schedulers.single())
@@ -195,13 +292,19 @@ public class SearchDialog extends CommonSearchDialog {
}
private Flowable<JNode> prepareSearch(String text) {
if (text == null || text.isEmpty() || options.isEmpty()) {
if (text == null || options.isEmpty()) {
return Flowable.empty();
}
TextSearchIndex index = cache.getTextIndex();
// allow empty text for comments search
if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) {
return Flowable.empty();
}
TextSearchIndex index = checkIndex();
if (index == null) {
return Flowable.empty();
}
LOG.debug("search event: {}", text);
showSearchState();
return index.buildSearch(text, options);
}
@@ -214,9 +317,7 @@ public class SearchDialog extends CommonSearchDialog {
highlightTextUseRegex = options.contains(SearchOptions.USE_REGEX);
cache.setLastSearch(text);
if (textSearch) {
cache.setLastSearchOptions(options);
}
cache.getLastSearchOptions().put(searchPreset, options);
resultsModel.clear();
resultsModel.addAll(results);
@@ -266,14 +367,6 @@ public class SearchDialog extends CommonSearchDialog {
.distinctUntilChanged();
}
@Override
public void dispose() {
if (searchDisposable != null && !searchDisposable.isDisposed()) {
searchDisposable.dispose();
}
super.dispose();
}
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
final JCheckBox chBox = new JCheckBox(name);
chBox.setAlignmentX(LEFT_ALIGNMENT);
@@ -291,23 +384,32 @@ public class SearchDialog extends CommonSearchDialog {
@Override
protected void loadFinished() {
if (!StringUtils.isEmpty(text)) {
searchField.setText(text);
}
resultsTable.setEnabled(true);
searchField.setEnabled(true);
searchEmitter.emitSearch();
}
@Override
protected void loadStart() {
text = cache.getLastSearch(); // SearchDialog is opened by menu item, let loadFinished to set text
cache.setLastSearch("");
resultsTable.setEnabled(false);
searchField.setEnabled(false);
}
public static void searchText(MainWindow window, String text) {
window.getCacheObject().setLastSearch(text);
new SearchDialog(window, true).setVisible(true);
private void registerActiveTabListener() {
removeActiveTabListener();
activeTabListener = e -> {
if (options.contains(SearchOptions.ACTIVE_TAB)) {
LOG.debug("active tab change event received");
searchEmitter.emitSearch();
}
};
mainWindow.getTabbedPane().addChangeListener(activeTabListener);
}
private void removeActiveTabListener() {
if (activeTabListener != null) {
mainWindow.getTabbedPane().removeChangeListener(activeTabListener);
activeTabListener = null;
}
}
}
@@ -1,11 +1,19 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.awt.Component;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.swing.*;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.Nullable;
@@ -19,14 +27,17 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.codearea.*;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition;
public class TabbedPane extends JTabbedPane {
private static final long serialVersionUID = -8833600618794570904L;
private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class);
private static final long serialVersionUID = -8833600618794570904L;
private final transient MainWindow mainWindow;
private final transient Map<JNode, ContentPanel> openTabs = new LinkedHashMap<>();
@@ -148,42 +159,42 @@ public class TabbedPane extends JTabbedPane {
return mainWindow;
}
private void showCode(final JumpPosition pos) {
final AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(pos.getNode());
private void showCode(final JumpPosition jumpPos) {
JNode jumpNode = jumpPos.getNode();
Objects.requireNonNull(jumpNode, "Null node in JumpPosition");
final AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(jumpNode);
if (contentPanel == null) {
return;
}
SwingUtilities.invokeLater(() -> {
setSelectedComponent(contentPanel);
AbstractCodeArea codeArea = contentPanel.getCodeArea();
if (pos.isPrecise()) {
codeArea.scrollToPos(pos.getPos());
int pos = jumpPos.getPos();
if (pos > 0) {
codeArea.scrollToPos(pos);
} else {
int line = pos.getLine();
int line = jumpPos.getLine();
if (line < 0) {
try {
line = 1 + codeArea.getLineOfOffset(-line);
} catch (BadLocationException e) {
LOG.error("Can't get line for: {}", pos, e);
line = pos.getNode().getLine();
LOG.error("Can't get line for: {}", jumpPos, e);
line = jumpNode.getLine();
}
}
if (pos.getPos() < 0) {
codeArea.scrollToLine(line);
} else {
int lineNum = Math.max(0, line - 1);
try {
int offs = codeArea.getLineStartOffset(lineNum);
while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
offs += 1;
}
offs += pos.getPos();
pos.setPrecise(offs);
codeArea.scrollToPos(offs);
} catch (BadLocationException e) {
e.printStackTrace();
codeArea.scrollToLine(line);
int lineNum = Math.max(0, line - 1);
try {
int offs = codeArea.getLineStartOffset(lineNum);
while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
offs += 1;
}
offs += pos;
jumpPos.setPos(offs);
codeArea.scrollToPos(offs);
} catch (BadLocationException e) {
e.printStackTrace();
codeArea.scrollToLine(line);
}
}
codeArea.requestFocus();
@@ -216,7 +227,7 @@ public class TabbedPane extends JTabbedPane {
}
@Nullable
JumpPosition getCurrentPosition() {
public JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedCodePanel();
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
return ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea().getCurrentPosition();
@@ -273,6 +284,7 @@ public class TabbedPane extends JTabbedPane {
ContentPanel panel = openTabs.get(node);
if (panel != null) {
setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel));
fireStateChanged();
}
}
@@ -1,14 +1,25 @@
package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.event.*;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.AbstractAction;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rtextarea.SearchContext;
@@ -19,9 +30,11 @@ import org.slf4j.LoggerFactory;
import jadx.core.utils.StringUtils;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.ContentPanel;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
@@ -66,21 +79,11 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
});
popupMenu.add(wrapItem);
popupMenu.addPopupMenuListener(new PopupMenuListener() {
popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
wrapItem.setState(getLineWrap());
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
Caret caret = getCaret();
@@ -305,9 +308,14 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
public JumpPosition getCurrentPosition() {
JumpPosition jp = new JumpPosition(node, getCaretLineNumber() + 1);
jp.setPrecise(getCaretPosition());
return jp;
return new JumpPosition(node, getCaretLineNumber() + 1, getCaretPosition());
}
public String getLineText(int line) throws BadLocationException {
int lineNum = line - 1;
int startOffset = getLineStartOffset(lineNum);
int endOffset = getLineEndOffset(lineNum);
return getText(startOffset, endOffset - startOffset);
}
@Nullable
@@ -322,4 +330,12 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
public JNode getNode() {
return node;
}
@Nullable
public JClass getJClass() {
if (node instanceof JClass) {
return (JClass) node;
}
return null;
}
}
@@ -1,8 +1,8 @@
package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.BorderLayout;
import javax.swing.*;
import javax.swing.JTabbedPane;
import javax.swing.border.EmptyBorder;
import jadx.gui.treemodel.JNode;
@@ -86,5 +86,4 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel {
public AbstractCodeArea getSmaliCodeArea() {
return smaliCodePanel.getCodeArea();
}
}
@@ -1,9 +1,12 @@
package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.event.*;
import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.Token;
@@ -15,12 +18,17 @@ import org.slf4j.LoggerFactory;
import jadx.api.CodePosition;
import jadx.api.JadxDecompiler;
import jadx.api.JavaNode;
import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.ContentPanel;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.CaretPositionFix;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.UiUtils;
/**
* The {@link AbstractCodeArea} implementation used for displaying Java code and text based
@@ -85,15 +93,33 @@ public final class CodeArea extends AbstractCodeArea {
FindUsageAction findUsage = new FindUsageAction(this);
GoToDeclarationAction goToDeclaration = new GoToDeclarationAction(this);
RenameAction rename = new RenameAction(this);
CommentAction comment = new CommentAction(this);
JPopupMenu popup = getPopupMenu();
popup.addSeparator();
popup.add(findUsage);
popup.add(goToDeclaration);
popup.add(comment);
popup.add(new CommentSearchAction(this));
popup.add(rename);
popup.addPopupMenuListener(findUsage);
popup.addPopupMenuListener(goToDeclaration);
popup.addPopupMenuListener(comment);
popup.addPopupMenuListener(rename);
// move caret on mouse right button click
popup.addPopupMenuListener(new DefaultPopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
CodeArea codeArea = CodeArea.this;
if (codeArea.getSelectedText() == null) {
int offset = UiUtils.getOffsetAtMousePosition(codeArea);
if (offset >= 0) {
codeArea.setCaretPosition(offset);
}
}
}
});
}
public int adjustOffsetForToken(@Nullable Token token) {
@@ -145,8 +171,7 @@ public final class CodeArea extends AbstractCodeArea {
return null;
}
JNode jNode = convertJavaNode(foundNode);
return new JumpPosition(jNode.getRootClass(), pos.getLine())
.setPrecise(JumpPosition.getDefPos(jNode));
return new JumpPosition(jNode.getRootClass(), pos.getLine(), JumpPosition.getDefPos(jNode));
}
private JNode convertJavaNode(JavaNode javaNode) {
@@ -189,11 +214,34 @@ public final class CodeArea extends AbstractCodeArea {
return null;
}
public void refreshClass() {
if (node instanceof JClass) {
JClass cls = (JClass) node;
try {
CaretPositionFix caretFix = new CaretPositionFix(this);
caretFix.save();
cls.reload();
IndexJob.refreshIndex(getMainWindow().getCacheObject(), cls.getCls());
ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel;
codeContentPanel.getTabbedPane().refresh(cls);
codeContentPanel.getJavaCodePanel().refresh(caretFix);
} catch (Exception e) {
LOG.error("Failed to reload class: {}", cls.getFullName(), e);
}
}
}
public MainWindow getMainWindow() {
return contentPanel.getTabbedPane().getMainWindow();
}
private JadxDecompiler getDecompiler() {
public JadxDecompiler getDecompiler() {
return getMainWindow().getWrapper().getDecompiler();
}
public JadxProject getProject() {
return getMainWindow().getProject();
}
}
@@ -1,25 +1,35 @@
package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.BorderLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.swing.*;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JPopupMenu.Separator;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import org.fife.ui.rsyntaxtextarea.Token;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.core.utils.StringUtils;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.CaretPositionFix;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -27,11 +37,13 @@ import jadx.gui.utils.UiUtils;
* 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 LineNumbers lineNumbers;
public CodePanel(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
@@ -71,7 +83,7 @@ public class CodePanel extends JPanel {
globalSearchItem.setAction(globalSearchAction);
Separator separator = new Separator();
JPopupMenu popupMenu = codeArea.getPopupMenu();
popupMenu.addPopupMenuListener(new PopupMenuListener() {
popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
String preferText = codeArea.getSelectedText();
@@ -90,18 +102,7 @@ public class CodePanel extends JPanel {
popupMenu.remove(searchItem);
}
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
}
public void loadSettings() {
@@ -115,9 +116,13 @@ public class CodePanel extends JPanel {
}
private void initLineNumbers() {
LineNumbers numbers = new LineNumbers(codeArea);
numbers.setUseSourceLines(isUseSourceLines());
codeScrollPane.setRowHeaderView(numbers);
initLineNumbers(isUseSourceLines());
}
private void initLineNumbers(boolean useSourceLines) {
lineNumbers = new LineNumbers(codeArea);
lineNumbers.setUseSourceLines(useSourceLines);
codeScrollPane.setRowHeaderView(lineNumbers);
}
private boolean isUseSourceLines() {
@@ -148,79 +153,15 @@ public class CodePanel extends JPanel {
return codeScrollPane;
}
public void refresh() {
int line = 0;
int tokenIndex;
int pos = codeArea.getCaretPosition();
int lineCount = codeArea.getLineCount();
try {
// after rename the change of document is undetectable, so
// use Token offset to calculate the new caret position.
line = codeArea.getLineOfOffset(pos);
Token token = codeArea.getTokenListForLine(line);
tokenIndex = getTokenIndexByOffset(token, pos);
} catch (BadLocationException e) {
e.printStackTrace();
tokenIndex = -1;
}
if (tokenIndex == -1) {
refreshToViewport();
return;
}
codeArea.refresh();
initLineNumbers();
int lineDiff = codeArea.getLineCount() - lineCount;
if (lineDiff > 0) {
lineDiff--;
} else if (lineDiff < 0) {
lineDiff++;
}
Token token = codeArea.getTokenListForLine(line + lineDiff);
int newPos = getOffsetOfTokenByIndex(tokenIndex, token);
SwingUtilities.invokeLater(() -> {
if (newPos != -1) {
codeArea.scrollToPos(newPos);
} else {
codeArea.scrollToLine(codeArea.getLineCount() - 1);
}
});
}
private void refreshToViewport() {
public void refresh(CaretPositionFix caretFix) {
JViewport viewport = getCodeScrollPane().getViewport();
Point viewPosition = viewport.getViewPosition();
codeArea.refresh();
initLineNumbers();
initLineNumbers(lineNumbers.isUseSourceLines());
SwingUtilities.invokeLater(() -> {
viewport.setViewPosition(viewPosition);
caretFix.restore();
});
}
private int getTokenIndexByOffset(Token token, int offset) {
if (token != null) {
int index = 1;
while (token.getEndOffset() < offset) {
token = token.getNextToken();
if (token == null) {
return -1;
}
index++;
}
return index;
}
return -1;
}
private int getOffsetOfTokenByIndex(int index, Token token) {
if (token != null && index != -1) {
for (int i = 0; i < index; i++) {
token = token.getNextToken();
if (token == null) {
return -1;
}
}
return token.getOffset();
}
return -1;
}
}
@@ -0,0 +1,153 @@
package jadx.gui.ui.codearea;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
import javax.swing.event.PopupMenuEvent;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.CodePosition;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.data.ICodeComment;
import jadx.api.data.annotations.CustomOffsetRef;
import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.data.impl.JadxCodeComment;
import jadx.api.data.impl.JadxNodeRef;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.CommentDialog;
import jadx.gui.utils.CodeLinesInfo;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import static javax.swing.KeyStroke.getKeyStroke;
public class CommentAction extends AbstractAction implements DefaultPopupMenuListener {
private static final long serialVersionUID = 4753838562204629112L;
private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class);
private final CodeArea codeArea;
private final JavaClass topCls;
private ICodeComment actionComment;
public CommentAction(CodeArea codeArea) {
super(NLS.str("popup.add_comment") + " (;)");
this.codeArea = codeArea;
JNode topNode = codeArea.getNode();
if (topNode instanceof JClass) {
this.topCls = ((JClass) topNode).getCls();
} else {
this.topCls = null;
}
KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, 0);
codeArea.getInputMap().put(key, "popup.add_comment");
codeArea.getActionMap().put("popup.add_comment", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
int line = codeArea.getCaretLineNumber() + 1;
ICodeComment codeComment = getCommentRef(line);
showCommentDialog(codeComment);
}
});
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
ICodeComment codeComment = getCommentRef(getMouseLine());
setEnabled(codeComment != null);
this.actionComment = codeComment;
}
@Override
public void actionPerformed(ActionEvent e) {
showCommentDialog(this.actionComment);
}
private void showCommentDialog(ICodeComment codeComment) {
if (codeComment == null) {
UiUtils.showMessageBox(codeArea.getMainWindow(), NLS.str("msg.cant_add_comment"));
return;
}
CommentDialog.show(codeArea, codeComment);
}
/**
* Check if possible insert comment at current line.
*
* @return blank code comment object (comment string empty)
*/
@Nullable
private ICodeComment getCommentRef(int line) {
if (line == -1 || this.topCls == null) {
return null;
}
try {
CodeLinesInfo linesInfo = new CodeLinesInfo(topCls, true); // TODO: cache and update on class refresh
// add comment if node definition at this line
JavaNode nodeAtLine = linesInfo.getDefAtLine(line);
if (nodeAtLine != null) {
// at node definition -> add comment for it
JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(nodeAtLine);
return new JadxCodeComment(nodeRef, "");
}
Object ann = topCls.getAnnotationAt(new CodePosition(line));
if (ann == null) {
// check if line with comment above node definition
try {
JavaNode defNode = linesInfo.getJavaNodeBelowLine(line);
if (defNode != null) {
String lineStr = codeArea.getLineText(line).trim();
if (lineStr.startsWith("//")) {
return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), "");
}
}
} catch (Exception e) {
LOG.error("Failed to check comment line: " + line, e);
}
return null;
}
// try to add method line comment
JavaNode node = linesInfo.getJavaNodeByLine(line);
if (node instanceof JavaMethod) {
JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node);
if (ann instanceof InsnCodeOffset) {
int rawOffset = ((InsnCodeOffset) ann).getOffset();
return new JadxCodeComment(nodeRef, "", rawOffset);
}
if (ann instanceof CustomOffsetRef) {
CustomOffsetRef customRef = (CustomOffsetRef) ann;
JadxCodeComment comment = new JadxCodeComment(nodeRef, "", customRef.getOffset());
comment.setAttachType(customRef.getAttachType());
return comment;
}
}
} catch (Exception e) {
LOG.error("Failed to add comment at line: " + line, e);
}
return null;
}
private int getMouseLine() {
int closestOffset = UiUtils.getOffsetAtMousePosition(codeArea);
if (closestOffset == -1) {
return -1;
}
try {
return codeArea.getLineOfOffset(closestOffset) + 1;
} catch (Exception e) {
LOG.debug("Failed to get line by offset: {}", closestOffset);
return -1;
}
}
}
@@ -0,0 +1,44 @@
package jadx.gui.ui.codearea;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.KeyStroke;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import static javax.swing.KeyStroke.getKeyStroke;
public class CommentSearchAction extends AbstractAction {
private static final long serialVersionUID = -3646341661734961590L;
private final CodeArea codeArea;
public CommentSearchAction(CodeArea codeArea) {
this.codeArea = codeArea;
KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton());
putValue(Action.NAME, NLS.str("popup.search_comment") + " (Ctrl + ;)");
codeArea.getInputMap().put(key, "popup.search_comment");
codeArea.getActionMap().put("popup.search_comment", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
startSearch();
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
startSearch();
}
private void startSearch() {
SearchDialog.searchInActiveTab(codeArea.getMainWindow(), SearchDialog.SearchPreset.COMMENT);
}
}
@@ -1,15 +1,17 @@
package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.Point;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.AbstractAction;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import org.fife.ui.rsyntaxtextarea.Token;
import org.jetbrains.annotations.Nullable;
import jadx.gui.utils.UiUtils;
public abstract class JNodeMenuAction<T> extends AbstractAction implements PopupMenuListener {
private static final long serialVersionUID = -2600154727884853550L;
@@ -36,8 +38,7 @@ public abstract class JNodeMenuAction<T> extends AbstractAction implements Popup
@Nullable
private T getNode() {
Point pos = MouseInfo.getPointerInfo().getLocation();
SwingUtilities.convertPointFromScreen(pos, codeArea);
Point pos = UiUtils.getMousePosition(codeArea);
Token token = codeArea.viewToToken(pos);
int offset = codeArea.adjustOffsetForToken(token);
return getNodeByOffset(offset);
@@ -107,6 +107,7 @@ public class LineNumbers extends JPanel implements CaretListener {
}
}
@SuppressWarnings("deprecation")
@Override
public void paintComponent(Graphics g) {
codeInfo = codeArea.getNode().getCodeInfo();
@@ -265,6 +266,10 @@ public class LineNumbers extends JPanel implements CaretListener {
}
}
public boolean isUseSourceLines() {
return useSourceLines;
}
public void setUseSourceLines(boolean useSourceLines) {
this.useSourceLines = useSourceLines;
}
@@ -1,6 +1,7 @@
package jadx.gui.utils;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
@@ -10,6 +11,7 @@ import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.search.CommentsIndex;
import jadx.gui.utils.search.TextSearchIndex;
public class CacheObject {
@@ -19,9 +21,11 @@ public class CacheObject {
private TextSearchIndex textIndex;
private CodeUsageInfo usageInfo;
private CommentsIndex commentsIndex;
private String lastSearch;
private JNodeCache jNodeCache;
private Set<SearchDialog.SearchOptions> lastSearchOptions;
private Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> lastSearchOptions;
private JRoot jRoot;
private JadxSettings settings;
@@ -38,7 +42,7 @@ public class CacheObject {
lastSearch = null;
jNodeCache = new JNodeCache();
usageInfo = null;
lastSearchOptions = EnumSet.noneOf(SearchDialog.SearchOptions.class);
lastSearchOptions = new HashMap<>();
}
public DecompileJob getDecompileJob() {
@@ -49,7 +53,6 @@ public class CacheObject {
this.decompileJob = decompileJob;
}
@Nullable
public TextSearchIndex getTextIndex() {
return textIndex;
}
@@ -76,6 +79,14 @@ public class CacheObject {
this.usageInfo = usageInfo;
}
public CommentsIndex getCommentsIndex() {
return commentsIndex;
}
public void setCommentsIndex(CommentsIndex commentsIndex) {
this.commentsIndex = commentsIndex;
}
public IndexJob getIndexJob() {
return indexJob;
}
@@ -88,11 +99,7 @@ public class CacheObject {
return jNodeCache;
}
public void setLastSearchOptions(Set<SearchDialog.SearchOptions> lastSearchOptions) {
this.lastSearchOptions = lastSearchOptions;
}
public Set<SearchDialog.SearchOptions> getLastSearchOptions() {
public Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> getLastSearchOptions() {
return lastSearchOptions;
}
@@ -0,0 +1,183 @@
package jadx.gui.utils;
import java.util.Map;
import org.fife.ui.rsyntaxtextarea.Token;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.CodePosition;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.data.annotations.ICodeRawOffset;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.codearea.AbstractCodeArea;
/**
* After class refresh (rename, comment, etc) the change of document is undetectable.
* So use Token index or offset in line to calculate the new caret position.
*/
public class CaretPositionFix {
private static final Logger LOG = LoggerFactory.getLogger(CaretPositionFix.class);
private final AbstractCodeArea codeArea;
private int linesCount;
private int line;
private int lineOffset;
private TokenInfo tokenInfo;
private int javaNodeLine = -1;
private int codeRawOffset = -1;
public CaretPositionFix(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
}
/**
* Save caret position by anchor to token under caret
*/
public void save() {
try {
linesCount = codeArea.getLineCount();
int pos = codeArea.getCaretPosition();
line = codeArea.getLineOfOffset(pos);
lineOffset = pos - codeArea.getLineStartOffset(line);
tokenInfo = getTokenInfoByOffset(codeArea.getTokenListForLine(line), pos);
JClass cls = codeArea.getJClass();
if (cls != null) {
JavaClass topParentClass = cls.getJavaNode().getTopParentClass();
Object ann = topParentClass.getAnnotationAt(new CodePosition(line));
if (ann instanceof ICodeRawOffset) {
codeRawOffset = ((ICodeRawOffset) ann).getOffset();
CodeLinesInfo codeLinesInfo = new CodeLinesInfo(topParentClass);
JavaNode javaNodeAtLine = codeLinesInfo.getJavaNodeByLine(line);
if (javaNodeAtLine != null) {
javaNodeLine = javaNodeAtLine.getDecompiledLine();
}
}
}
LOG.debug("Saved position data: line={}, lineOffset={}, token={}, codeRawOffset={}, javaNodeLine={}",
line, lineOffset, tokenInfo, codeRawOffset, javaNodeLine);
} catch (Exception e) {
LOG.error("Failed to save caret position before refresh", e);
line = -1;
}
}
/**
* Restore caret position in refreshed code.
* Expected to be called in UI thread.
*/
public void restore() {
if (line == -1) {
return;
}
try {
int newLine = getNewLine();
int lineStartOffset = codeArea.getLineStartOffset(newLine);
int lineEndOffset = codeArea.getLineEndOffset(newLine) - 1;
int lineLength = lineEndOffset - lineStartOffset;
Token token = codeArea.getTokenListForLine(newLine);
int newPos = getOffsetFromTokenInfo(tokenInfo, token);
if (newPos == -1) {
// can't restore using token -> just restore by line offset
if (lineOffset < lineLength) {
newPos = lineStartOffset + lineOffset;
} else {
// line truncated -> set caret at line end
newPos = lineEndOffset;
}
}
codeArea.setCaretPosition(newPos);
LOG.debug("Restored caret position: {}, line: {}", newPos, newLine);
} catch (Exception e) {
LOG.warn("Failed to restore caret position", e);
}
}
private int getNewLine() {
int newLinesCount = codeArea.getLineCount();
if (linesCount == newLinesCount) {
return line;
}
// lines count changes, try find line by raw offset
if (javaNodeLine != -1) {
JClass cls = codeArea.getJClass();
if (cls != null) {
JavaClass topParentClass = cls.getJavaNode().getTopParentClass();
for (Map.Entry<CodePosition, Object> entry : topParentClass.getCodeAnnotations().entrySet()) {
CodePosition pos = entry.getKey();
if (pos.getOffset() == 0 && pos.getLine() >= javaNodeLine) {
Object ann = entry.getValue();
if (ann instanceof ICodeRawOffset && ((ICodeRawOffset) ann).getOffset() == codeRawOffset) {
return pos.getLine() - 1;
}
}
}
}
}
// fallback: assume lines added/removed before caret
return line - (linesCount - newLinesCount);
}
private TokenInfo getTokenInfoByOffset(Token token, int offset) {
if (token == null) {
return null;
}
int index = 1;
while (token.getEndOffset() < offset) {
token = token.getNextToken();
if (token == null) {
return null;
}
index++;
}
return new TokenInfo(index, token.getType());
}
private int getOffsetFromTokenInfo(TokenInfo tokenInfo, Token token) {
if (tokenInfo == null || token == null) {
return -1;
}
int index = tokenInfo.getIndex();
if (index == -1) {
return -1;
}
for (int i = 0; i < index; i++) {
token = token.getNextToken();
if (token == null) {
return -1;
}
}
if (token.getType() != tokenInfo.getType()) {
return -1;
}
return token.getOffset();
}
private static final class TokenInfo {
private final int index;
private final int type;
public TokenInfo(int index, int type) {
this.index = index;
this.type = type;
}
public int getIndex() {
return index;
}
public int getType() {
return type;
}
@Override
public String toString() {
return "Token{index=" + index + ", type=" + type + '}';
}
}
}
@@ -5,6 +5,7 @@ import java.util.NavigableMap;
import java.util.TreeMap;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
@@ -12,18 +13,27 @@ public class CodeLinesInfo {
private final NavigableMap<Integer, JavaNode> map = new TreeMap<>();
public CodeLinesInfo(JavaClass cls) {
addClass(cls);
addClass(cls, false);
}
public void addClass(JavaClass cls) {
public CodeLinesInfo(JavaClass cls, boolean includeFields) {
addClass(cls, includeFields);
}
private void addClass(JavaClass cls, boolean includeFields) {
map.put(cls.getDecompiledLine(), cls);
for (JavaClass innerCls : cls.getInnerClasses()) {
map.put(innerCls.getDecompiledLine(), innerCls);
addClass(innerCls);
addClass(innerCls, includeFields);
}
for (JavaMethod mth : cls.getMethods()) {
map.put(mth.getDecompiledLine(), mth);
}
if (includeFields) {
for (JavaField field : cls.getFields()) {
map.put(field.getDecompiledLine(), field);
}
}
}
public JavaNode getJavaNodeByLine(int line) {
@@ -33,4 +43,16 @@ public class CodeLinesInfo {
}
return entry.getValue();
}
public JavaNode getJavaNodeBelowLine(int line) {
Map.Entry<Integer, JavaNode> entry = map.ceilingEntry(line);
if (entry == null) {
return null;
}
return entry.getValue();
}
public JavaNode getDefAtLine(int line) {
return map.get(line);
}
}
@@ -64,7 +64,7 @@ public class CodeUsageInfo {
JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line);
StringRef codeLine = lines.get(line - 1);
JNode node = nodeCache.makeFrom(javaNodeByLine == null ? javaClass : javaNodeByLine);
CodeNode codeNode = new CodeNode(node, line, codeLine).setPrecisePos(codePosition.getUsagePosition());
CodeNode codeNode = new CodeNode(node, codeLine, line, codePosition.getPos());
usageInfo.addUsage(codeNode);
}
@@ -0,0 +1,18 @@
package jadx.gui.utils;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
public interface DefaultPopupMenuListener extends PopupMenuListener {
@Override
default void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
@Override
default void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
default void popupMenuCanceled(PopupMenuEvent e) {
}
}
@@ -1,17 +1,20 @@
package jadx.gui.utils;
import jadx.api.*;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.*;
import jadx.api.CodePosition;
import jadx.api.JavaNode;
import jadx.gui.treemodel.JNode;
public class JumpPosition {
private final JNode node;
private final int line;
private int pos;
private boolean precise;
public JumpPosition(JNode node, int line) {
this(node, line, -1);
public JumpPosition(JNode jumpNode) {
this(jumpNode.getRootClass(), jumpNode.getLine(), jumpNode.getPos());
}
public JumpPosition(JNode jumpNode, CodePosition codePos) {
this(jumpNode.getRootClass(), codePos.getLine(), codePos.getPos());
}
public JumpPosition(JNode node, int line, int pos) {
@@ -20,20 +23,14 @@ public class JumpPosition {
this.pos = pos;
}
public boolean isPrecise() {
return precise;
}
public JumpPosition setPrecise(int pos) {
this.pos = pos;
this.precise = true;
return this;
}
public int getPos() {
return pos;
}
public void setPos(int pos) {
this.pos = pos;
}
public JNode getNode() {
return node;
}
@@ -43,35 +40,11 @@ public class JumpPosition {
}
public static int getDefPos(JNode node) {
if (node instanceof JClass) {
return ((JClass) node).getCls().getClassNode().getDefPosition();
JavaNode javaNode = node.getJavaNode();
if (javaNode == null) {
return -1;
}
if (node instanceof JMethod) {
return ((JMethod) node).getJavaMethod().getMethodNode().getDefPosition();
}
if (node instanceof JField) {
return ((JField) node).getJavaField().getFieldNode().getDefPosition();
}
if (node instanceof JVariable) {
return ((JVariable) node).getJavaVarNode().getVariableNode().getDefPosition();
}
throw new JadxRuntimeException("Unexpected node " + node);
}
public static int getDefPos(JavaNode node) {
if (node instanceof JavaClass) {
return ((JavaClass) node).getClassNode().getDefPosition();
}
if (node instanceof JavaMethod) {
return ((JavaMethod) node).getMethodNode().getDefPosition();
}
if (node instanceof JavaField) {
return ((JavaField) node).getFieldNode().getDefPosition();
}
if (node instanceof JavaVariable) {
return ((JavaVariable) node).getVariableNode().getDefPosition();
}
throw new JadxRuntimeException("Unexpected node " + node);
return javaNode.getDefPos();
}
@Override
@@ -11,7 +11,6 @@ import javax.swing.*;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;
@SuppressWarnings("serial")
public class TextStandardActions {
private final JTextComponent textComponent;
@@ -27,6 +26,10 @@ public class TextStandardActions {
private Action deleteAction;
private Action selectAllAction;
public static void attach(JTextComponent textComponent) {
new TextStandardActions(textComponent);
}
public TextStandardActions(JTextComponent textComponent) {
this.textComponent = textComponent;
this.undoManager = new UndoManager();
@@ -1,6 +1,11 @@
package jadx.gui.utils;
import java.awt.*;
import java.awt.Component;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
@@ -10,7 +15,14 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.intellij.lang.annotations.MagicConstant;
import org.slf4j.Logger;
@@ -19,6 +31,7 @@ import org.slf4j.LoggerFactory;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.codearea.AbstractCodeArea;
public class UiUtils {
private static final Logger LOG = LoggerFactory.getLogger(UiUtils.class);
@@ -190,7 +203,7 @@ public class UiUtils {
@SuppressWarnings("deprecation")
@MagicConstant(flagsFromClass = InputEvent.class)
private static int getCtrlButton() {
if (System.getProperty("os.name").toLowerCase().contains("mac")) {
if (SystemInfo.IS_MAC) {
return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
} else {
return InputEvent.CTRL_DOWN_MASK;
@@ -210,4 +223,26 @@ public class UiUtils {
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
}
/**
* Get closest offset at mouse position
*
* @return -1 on error
*/
@SuppressWarnings("deprecation")
public static int getOffsetAtMousePosition(AbstractCodeArea codeArea) {
try {
Point mousePos = getMousePosition(codeArea);
return codeArea.viewToModel(mousePos);
} catch (Exception e) {
LOG.error("Failed to get offset at mouse position", e);
return -1;
}
}
public static Point getMousePosition(Component comp) {
Point pos = MouseInfo.getPointerInfo().getLocation();
SwingUtilities.convertPointFromScreen(pos, comp);
return pos;
}
}
@@ -0,0 +1,187 @@
package jadx.gui.utils.layout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
/**
* FlowLayout subclass that fully supports wrapping of components.
*/
public class WrapLayout extends FlowLayout {
private static final long serialVersionUID = 6109752116520941346L;
private Dimension preferredLayoutSize;
/**
* Constructs a new <code>WrapLayout</code> with a left
* alignment and a default 5-unit horizontal and vertical gap.
*/
public WrapLayout() {
super();
}
/**
* Constructs a new <code>FlowLayout</code> with the specified
* alignment and a default 5-unit horizontal and vertical gap.
* The value of the alignment argument must be one of
* <code>WrapLayout</code>, <code>WrapLayout</code>,
* or <code>WrapLayout</code>.
*
* @param align the alignment value
*/
public WrapLayout(int align) {
super(align);
}
/**
* Creates a new flow layout manager with the indicated alignment
* and the indicated horizontal and vertical gaps.
* <p>
* The value of the alignment argument must be one of
* <code>WrapLayout</code>, <code>WrapLayout</code>,
* or <code>WrapLayout</code>.
*
* @param align the alignment value
* @param hgap the horizontal gap between components
* @param vgap the vertical gap between components
*/
public WrapLayout(int align, int hgap, int vgap) {
super(align, hgap, vgap);
}
/**
* Returns the preferred dimensions for this layout given the
* <i>visible</i> components in the specified target container.
*
* @param target the component which needs to be laid out
* @return the preferred dimensions to lay out the
* subcomponents of the specified container
*/
@Override
public Dimension preferredLayoutSize(Container target) {
return layoutSize(target, true);
}
/**
* Returns the minimum dimensions needed to layout the <i>visible</i>
* components contained in the specified target container.
*
* @param target the component which needs to be laid out
* @return the minimum dimensions to lay out the
* subcomponents of the specified container
*/
@Override
public Dimension minimumLayoutSize(Container target) {
Dimension minimum = layoutSize(target, false);
minimum.width -= (getHgap() + 1);
return minimum;
}
/**
* Returns the minimum or preferred dimension needed to layout the target
* container.
*
* @param target target to get layout size for
* @param preferred should preferred size be calculated
* @return the dimension to layout the target container
*/
private Dimension layoutSize(Container target, boolean preferred) {
synchronized (target.getTreeLock()) {
// Each row must fit with the width allocated to the containter.
// When the container width = 0, the preferred width of the container
// has not yet been calculated so lets ask for the maximum.
int targetWidth = target.getSize().width;
Container container = target;
while (container.getSize().width == 0 && container.getParent() != null) {
container = container.getParent();
}
targetWidth = container.getSize().width;
if (targetWidth == 0) {
targetWidth = Integer.MAX_VALUE;
}
int hgap = getHgap();
int vgap = getVgap();
Insets insets = target.getInsets();
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
int maxWidth = targetWidth - horizontalInsetsAndGap;
// Fit components into the allowed width
Dimension dim = new Dimension(0, 0);
int rowWidth = 0;
int rowHeight = 0;
int nmembers = target.getComponentCount();
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
// Can't add the component to current row. Start a new row.
if (rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight);
rowWidth = 0;
rowHeight = 0;
}
// Add a horizontal gap for all components after the first
if (rowWidth != 0) {
rowWidth += hgap;
}
rowWidth += d.width;
rowHeight = Math.max(rowHeight, d.height);
}
}
addRow(dim, rowWidth, rowHeight);
dim.width += horizontalInsetsAndGap;
dim.height += insets.top + insets.bottom + vgap * 2;
// When using a scroll pane or the DecoratedLookAndFeel we need to
// make sure the preferred size is less than the size of the
// target containter so shrinking the container size works
// correctly. Removing the horizontal gap is an easy way to do this.
Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
if (scrollPane != null && target.isValid()) {
dim.width -= (hgap + 1);
}
return dim;
}
}
/*
* A new row has been completed. Use the dimensions of this row
* to update the preferred size for the container.
* @param dim update the width and height when appropriate
* @param rowWidth the width of the row to add
* @param rowHeight the height of the row to add
*/
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
dim.width = Math.max(dim.width, rowWidth);
if (dim.height > 0) {
dim.height += getVgap();
}
dim.height += rowHeight;
}
}
@@ -11,6 +11,7 @@ import io.reactivex.Flowable;
import jadx.api.JavaClass;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.utils.UiUtils;
public class CodeIndex {
@@ -32,13 +33,15 @@ public class CodeIndex {
}
public Flowable<CodeNode> search(final SearchSettings searchSettings) {
JClass activeCls = searchSettings.getActiveCls();
return Flowable.create(emitter -> {
LOG.debug("Code search started: {} ...", searchSettings.getSearchString());
for (CodeNode node : values) {
int pos = searchSettings.find(node.getLineStr());
node.setPos(pos);
if (pos > -1) {
emitter.onNext(node);
if (activeCls == null || node.getRootClass().equals(activeCls)) {
int pos = searchSettings.find(node.getLineStr());
if (pos > -1) {
emitter.onNext(node);
}
}
if (emitter.isCancelled()) {
LOG.debug("Code search canceled: {}", searchSettings.getSearchString());
@@ -0,0 +1,230 @@
package jadx.gui.utils.search;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.swing.Icon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import jadx.api.CodePosition;
import jadx.api.ICodeInfo;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.data.ICodeComment;
import jadx.api.data.IJavaNodeRef;
import jadx.api.data.annotations.ICodeRawOffset;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
public class CommentsIndex {
private static final Logger LOG = LoggerFactory.getLogger(CommentsIndex.class);
private final JadxWrapper wrapper;
private final CacheObject cacheObject;
private final JadxProject project;
public CommentsIndex(JadxWrapper wrapper, CacheObject cacheObject, JadxProject project) {
this.wrapper = wrapper;
this.cacheObject = cacheObject;
this.project = project;
}
@Nullable
private JNode isMatch(SearchSettings searchSettings, ICodeComment comment) {
boolean all = searchSettings.getSearchString().isEmpty();
if (all || searchSettings.isMatch(comment.getComment())) {
JNode refNode = getRefNode(comment);
if (refNode != null) {
JClass activeCls = searchSettings.getActiveCls();
if (activeCls == null || Objects.equals(activeCls, refNode.getRootClass())) {
return getCommentNode(comment, refNode);
}
} else {
LOG.warn("Failed to get ref node for comment: {}", comment);
}
}
return null;
}
public Flowable<JNode> search(SearchSettings searchSettings) {
List<ICodeComment> comments = project.getCodeData().getComments();
if (comments == null || comments.isEmpty()) {
return Flowable.empty();
}
LOG.debug("Total comments count: {}", comments.size());
return Flowable.create(emitter -> {
for (ICodeComment comment : comments) {
JNode foundNode = isMatch(searchSettings, comment);
if (foundNode != null) {
emitter.onNext(foundNode);
}
if (emitter.isCancelled()) {
return;
}
}
emitter.onComplete();
}, BackpressureStrategy.BUFFER);
}
private @NotNull RefCommentNode getCommentNode(ICodeComment comment, JNode refNode) {
IJavaNodeRef nodeRef = comment.getNodeRef();
if (nodeRef.getType() == IJavaNodeRef.RefType.METHOD && comment.getOffset() > 0) {
return new CodeCommentNode((JMethod) refNode, comment);
}
return new RefCommentNode(refNode, comment.getComment());
}
@Nullable
private JNode getRefNode(ICodeComment comment) {
IJavaNodeRef nodeRef = comment.getNodeRef();
JavaClass javaClass = wrapper.searchJavaClassByOrigClassName(nodeRef.getDeclaringClass());
if (javaClass == null) {
return null;
}
JNodeCache nodeCache = cacheObject.getNodeCache();
switch (nodeRef.getType()) {
case CLASS:
return nodeCache.makeFrom(javaClass);
case FIELD:
for (JavaField field : javaClass.getFields()) {
if (field.getFieldNode().getFieldInfo().getShortId().equals(nodeRef.getShortId())) {
return nodeCache.makeFrom(field);
}
}
break;
case METHOD:
for (JavaMethod mth : javaClass.getMethods()) {
if (mth.getMethodNode().getMethodInfo().getShortId().equals(nodeRef.getShortId())) {
return nodeCache.makeFrom(mth);
}
}
break;
}
return null;
}
private static final class CodeCommentNode extends RefCommentNode {
private static final long serialVersionUID = 6208192811789176886L;
private final int offset;
private JumpPosition pos;
public CodeCommentNode(JMethod node, ICodeComment comment) {
super(node, comment.getComment());
this.offset = comment.getOffset();
}
@Override
public int getLine() {
return getCachedPos().getLine();
}
@Override
public int getPos() {
return getCachedPos().getPos();
}
private synchronized JumpPosition getCachedPos() {
if (pos == null) {
pos = getJumpPos();
}
return pos;
}
/**
* Lazy decompilation to get comment location if requested
*/
private JumpPosition getJumpPos() {
JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
int methodLine = javaMethod.getDecompiledLine();
ICodeInfo codeInfo = javaMethod.getTopParentClass().getCodeInfo();
for (Map.Entry<CodePosition, Object> entry : codeInfo.getAnnotations().entrySet()) {
CodePosition codePos = entry.getKey();
if (codePos.getOffset() == 0 && codePos.getLine() > methodLine) {
Object ann = entry.getValue();
if (ann instanceof ICodeRawOffset) {
if (((ICodeRawOffset) ann).getOffset() == offset) {
return new JumpPosition(node, codePos);
}
}
}
}
return new JumpPosition(node);
}
}
private static class RefCommentNode extends JNode {
private static final long serialVersionUID = 3887992236082515752L;
protected final JNode node;
protected final String comment;
public RefCommentNode(JNode node, String comment) {
this.node = node;
this.comment = comment;
}
@Override
public JClass getRootClass() {
return node.getRootClass();
}
@Override
public JavaNode getJavaNode() {
return node.getJavaNode();
}
@Override
public JClass getJParent() {
return node.getJParent();
}
@Override
public Icon getIcon() {
return node.getIcon();
}
@Override
public String getSyntaxName() {
return node.getSyntaxName();
}
@Override
public String makeString() {
return node.makeString();
}
@Override
public int getLine() {
return node.getLine();
}
@Override
public String makeDescString() {
return comment;
}
@Override
public boolean hasDescString() {
return true;
}
}
}
@@ -7,18 +7,19 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.treemodel.JClass;
public class SearchSettings {
private static final Logger LOG = LoggerFactory.getLogger(SearchSettings.class);
private final String searchString;
private final boolean useRegex;
private final boolean ignoreCase;
private Pattern regexPattern;
private JClass activeCls;
private Pattern regexPattern;
private int startPos = 0;
public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex) {
@@ -27,109 +28,81 @@ public class SearchSettings {
this.ignoreCase = ignoreCase;
}
/*
* Return whether Regex search should be done
*/
public boolean isUseRegex() {
return this.useRegex;
}
/*
* Return whether case will be ignored
*/
public boolean isIgnoreCase() {
return this.ignoreCase;
}
/*
* Return search string
*/
public String getSearchString() {
return this.searchString;
}
/*
* Return the starting index
*/
public int getStartPos() {
return this.startPos;
}
/*
* Set Starting Index
*/
public void setStartPos(int startPos) {
this.startPos = startPos;
}
/*
* get Regex Pattern
*/
public Pattern getPattern() {
return this.regexPattern;
}
/*
* Runs Pattern.compile if using Regex. If not using Regex return true
* return false is invalid Regex
*/
public boolean preCompile() {
try {
if (this.useRegex && this.ignoreCase) {
this.regexPattern = Pattern.compile(this.searchString, Pattern.CASE_INSENSITIVE);
} else if (this.useRegex) {
this.regexPattern = Pattern.compile(this.searchString);
if (useRegex) {
try {
int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
this.regexPattern = Pattern.compile(searchString, flags);
} catch (Exception e) {
LOG.warn("Invalid Regex: {}", this.searchString, e);
return false;
}
} catch (Exception e) {
LOG.warn("Invalid Regex: {}", this.searchString);
return false;
}
return true;
}
/*
* Checks if searchArea matches the searched string found in searchSettings
*/
public boolean isMatch(StringRef searchArea) {
return isMatch(searchArea.toString());
return find(searchArea) != -1;
}
/*
* Checks if searchArea matches the searched string found in searchSettings
*/
public boolean isMatch(String searchArea) {
return find(searchArea) != -1;
}
/*
* Returns the position within searchArea that the searched string found in searchSettings was
* identified.
* returns -1 if a match is not found
*/
public int find(StringRef searchArea) {
return find(searchArea.toString());
}
/*
* Returns the position within searchArea that the searched string found in searchSettings was
* identified.
* returns -1 if a match is not found
*/
public int find(String searchArea) {
int pos;
if (this.useRegex) {
Matcher matcher = this.regexPattern.matcher(searchArea);
if (matcher.find(this.startPos)) {
pos = matcher.start();
} else {
pos = -1;
}
} else if (this.ignoreCase) {
pos = StringUtils.indexOfIgnoreCase(searchArea, this.searchString, this.startPos);
} else {
pos = searchArea.indexOf(this.searchString, this.startPos);
if (useRegex) {
return findWithRegex(searchArea.toString());
}
return pos;
return searchArea.indexOf(this.searchString, this.startPos, this.ignoreCase);
}
public int find(String searchArea) {
if (useRegex) {
return findWithRegex(searchArea);
}
if (ignoreCase) {
return StringUtils.indexOfIgnoreCase(searchArea, searchString, startPos);
}
return searchArea.indexOf(searchString, startPos);
}
private int findWithRegex(String searchArea) {
Matcher matcher = regexPattern.matcher(searchArea);
if (matcher.find(startPos)) {
return matcher.start();
}
return -1;
}
public JClass getActiveCls() {
return activeCls;
}
public void setActiveCls(JClass activeCls) {
this.activeCls = activeCls;
}
}
@@ -1,12 +1,14 @@
package jadx.gui.utils.search;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import jadx.api.JavaClass;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
public class SimpleIndex {
@@ -20,15 +22,23 @@ public class SimpleIndex {
data.entrySet().removeIf(e -> e.getKey().getJavaNode().getTopParentClass().equals(cls));
}
private boolean isMatched(String str, SearchSettings searchSettings) {
return searchSettings.isMatch(str);
private boolean isMatched(String str, JNode node, SearchSettings searchSettings) {
if (searchSettings.isMatch(str)) {
JClass activeCls = searchSettings.getActiveCls();
if (activeCls == null) {
return true;
}
return Objects.equals(node.getRootClass(), activeCls);
}
return false;
}
public Flowable<JNode> search(final SearchSettings searchSettings) {
return Flowable.create(emitter -> {
for (Map.Entry<JNode, String> entry : data.entrySet()) {
if (isMatched(entry.getValue(), searchSettings)) {
emitter.onNext(entry.getKey());
JNode node = entry.getKey();
if (isMatched(entry.getValue(), node, searchSettings)) {
emitter.onNext(node);
}
if (emitter.isCancelled()) {
return;
@@ -18,24 +18,30 @@ import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.CodeLinesInfo;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.UiUtils;
import static jadx.gui.ui.SearchDialog.SearchOptions.ACTIVE_TAB;
import static jadx.gui.ui.SearchDialog.SearchOptions.CLASS;
import static jadx.gui.ui.SearchDialog.SearchOptions.CODE;
import static jadx.gui.ui.SearchDialog.SearchOptions.COMMENT;
import static jadx.gui.ui.SearchDialog.SearchOptions.FIELD;
import static jadx.gui.ui.SearchDialog.SearchOptions.IGNORE_CASE;
import static jadx.gui.ui.SearchDialog.SearchOptions.METHOD;
import static jadx.gui.ui.SearchDialog.SearchOptions.Resource;
import static jadx.gui.ui.SearchDialog.SearchOptions.RESOURCE;
import static jadx.gui.ui.SearchDialog.SearchOptions.USE_REGEX;
public class TextSearchIndex {
private static final Logger LOG = LoggerFactory.getLogger(TextSearchIndex.class);
private final CacheObject cache;
private final MainWindow mainWindow;
private final JNodeCache nodeCache;
private final SimpleIndex clsNamesIndex;
@@ -46,7 +52,9 @@ public class TextSearchIndex {
private final List<JavaClass> skippedClasses = new ArrayList<>();
public TextSearchIndex(CacheObject cache) {
public TextSearchIndex(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.cache = mainWindow.getCacheObject();
this.nodeCache = cache.getNodeCache();
this.resIndex = new ResourceIndex(cache);
this.clsNamesIndex = new SimpleIndex();
@@ -81,8 +89,9 @@ public class TextSearchIndex {
}
int lineNum = i + 1;
JavaNode node = linesInfo.getJavaNodeByLine(lineNum);
JNode nodeAtLine = nodeCache.makeFrom(node == null ? cls : node);
codeIndex.put(new CodeNode(nodeAtLine, lineNum, line));
JavaNode javaNode = node == null ? cls : node;
JNode nodeAtLine = nodeCache.makeFrom(javaNode);
codeIndex.put(new CodeNode(nodeAtLine, line, lineNum, javaNode.getDefPos()));
}
} catch (Exception e) {
LOG.warn("Failed to index class: {}", cls, e);
@@ -108,10 +117,29 @@ public class TextSearchIndex {
Flowable<JNode> result = Flowable.empty();
SearchSettings searchSettings = new SearchSettings(text, options.contains(IGNORE_CASE), options.contains(USE_REGEX));
if (options.contains(ACTIVE_TAB)) {
JumpPosition activeNode = mainWindow.getTabbedPane().getCurrentPosition();
if (activeNode != null) {
searchSettings.setActiveCls(activeNode.getNode().getRootClass());
}
if (searchSettings.getActiveCls() == null) {
return result;
}
}
if (!searchSettings.preCompile()) {
return result;
}
if (options.contains(COMMENT)) {
CommentsIndex commentsIndex = cache.getCommentsIndex();
result = Flowable.concat(result, commentsIndex.search(searchSettings));
if (text.isEmpty()) {
// return all comments on empty search string
// other searches don't support empty string, so return immediately
return result;
}
}
if (options.contains(CLASS)) {
result = Flowable.concat(result, clsNamesIndex.search(searchSettings));
}
@@ -129,7 +157,7 @@ public class TextSearchIndex {
result = Flowable.concat(result, searchInSkippedClasses(searchSettings));
}
}
if (options.contains(Resource)) {
if (options.contains(RESOURCE)) {
result = Flowable.concat(result, resIndex.search(searchSettings));
}
return result;
@@ -168,7 +196,7 @@ public class TextSearchIndex {
int lineStart = 1 + code.lastIndexOf(ICodeWriter.NL, pos);
int lineEnd = code.indexOf(ICodeWriter.NL, pos + searchSettings.getSearchString().length());
StringRef line = StringRef.subString(code, lineStart, lineEnd == -1 ? code.length() : lineEnd);
emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), -pos, line.trim()).setPos(pos));
emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), line.trim(), -1, pos));
return lineEnd;
}
@@ -11,6 +11,7 @@ menu.heapUsageBar=Speicherverbrauchsleiste anzeigen
menu.navigation=Navigation
menu.text_search=Textsuche
menu.class_search=Klassen-Suche
#menu.comment_search=Comment search
menu.tools=Tools
menu.deobfuscation=Deobfuscation
menu.log=Log-Anzeige
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=<html>Jadx hat nur noch wenig Speicherplatz. Dahe
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB
#common_dialog.ok=
#common_dialog.cancel=
#common_dialog.add=
#common_dialog.update=
#common_dialog.remove=
search_dialog.open=Öffnen
search_dialog.cancel=Beenden
search_dialog.open_by_name=Nach Text suchen:
@@ -77,6 +84,8 @@ search_dialog.info_label=Zeige Ergebnisse %1$d bis %2$d von %3$d
search_dialog.col_node=Knoten
search_dialog.col_code=Code
search_dialog.regex=Regex
#search_dialog.active_tab=Active tab only
#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=Regex
usage_dialog.title=Verwendungssuche
usage_dialog.label=Verwendung für:
#comment_dialog.title.add=Add code comment
#comment_dialog.title.update=Update code comment
#comment_dialog.label=Comment:
#comment_dialog.usage=
log_viewer.title=Log-Anzeige
log_viewer.log_level=Log-Level:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=Bitte aktivieren Sie die Umbenennung
msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht.
#msg.rename_node_disabled=
#msg.rename_node_failed=
#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=Alle auswählen
popup.find_usage=Verwendung suchen
popup.go_to_declaration=Zur Erklärung gehen
popup.exclude=Ausschließen
#popup.add_comment=Comment
#popup.search_comment=Search comments
popup.rename=Umbennen
#popup.search=
#popup.search_global=
@@ -11,6 +11,7 @@ menu.heapUsageBar=Show memory usage bar
menu.navigation=Navigation
menu.text_search=Text search
menu.class_search=Class search
menu.comment_search=Comment search
menu.tools=Tools
menu.deobfuscation=Deobfuscation
menu.log=Log Viewer
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=<html>Jadx is running low on memory. Therefore %d
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
common_dialog.ok=Ok
common_dialog.cancel=Cancel
common_dialog.add=Add
common_dialog.update=Update
common_dialog.remove=Remove
search_dialog.open=Open
search_dialog.cancel=Cancel
search_dialog.open_by_name=Search for text:
@@ -77,6 +84,8 @@ search_dialog.info_label=Showing results %1$d to %2$d of %3$d
search_dialog.col_node=Node
search_dialog.col_code=Code
search_dialog.regex=Regex
search_dialog.active_tab=Active tab only
search_dialog.comments=Comments
search_dialog.resource=Resource
search_dialog.keep_open=Keep open
search_dialog.tip_searching=Searching ...
@@ -84,6 +93,11 @@ search_dialog.tip_searching=Searching ...
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
comment_dialog.title.add=Add code comment
comment_dialog.title.update=Update code comment
comment_dialog.label=Comment:
comment_dialog.usage=Use Shift + Enter for start a new line
log_viewer.title=Log Viewer
log_viewer.log_level=Log level:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=Enable deobfuscation.
msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist.
msg.rename_node_disabled=Can't rename this node
msg.rename_node_failed=Can't rename %s
msg.cant_add_comment=Can't add comment here
popup.bytecode_col=Show Bytecode
popup.line_wrap=Line Wrap
@@ -168,6 +183,8 @@ popup.select_all=Select All
popup.find_usage=Find Usage
popup.go_to_declaration=Go to declaration
popup.exclude=Exclude
popup.add_comment=Comment
popup.search_comment=Search comments
popup.rename=Rename
popup.search=Search "%s"
popup.search_global=Global Search "%s"
@@ -11,6 +11,7 @@ menu.flatten=Mostrar paquetes en vista plana
menu.navigation=Navegación
menu.text_search=Buscar texto
menu.class_search=Buscar clase
#menu.comment_search=Comment search
menu.tools=Herramientas
menu.deobfuscation=Desofuscación
menu.log=Visor log
@@ -61,6 +62,12 @@ nav.forward=Adelante
#heapUsage.text=
#common_dialog.ok=Ok
#common_dialog.cancel=Cancel
#common_dialog.add=Add
#common_dialog.update=Update
#common_dialog.remove=Remove
search_dialog.open=Abrir
search_dialog.cancel=Cancelar
search_dialog.open_by_name=Buscar texto:
@@ -77,6 +84,8 @@ search_dialog.info_label=Mostrando resultados %1$d a %2$d de %3$d
search_dialog.col_node=Nodo
search_dialog.col_code=Código
search_dialog.regex=Regex
#search_dialog.active_tab=Active tab only
#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=Regex
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
#comment_dialog.title.add=Add code comment
#comment_dialog.title.update=Update code comment
#comment_dialog.label=Comment:
#comment_dialog.usage=
log_viewer.title=Visor log
log_viewer.log_level=Nivel log:
@@ -155,6 +169,7 @@ msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivar
#msg.cmd_select_class_error=
#msg.rename_node_disabled=
#msg.rename_node_failed=
#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=Seleccionar todo
#popup.find_usage=
#popup.go_to_declaration=
#popup.exclude=
#popup.add_comment=Comment
#popup.search_comment=Search comments
popup.rename=Nimeta ümber
#popup.search=
#popup.search_global=
@@ -11,6 +11,7 @@ menu.heapUsageBar=메모리 사용량 표시
menu.navigation=네비게이션
menu.text_search=텍스트 검색
menu.class_search=클래스 검색
#menu.comment_search=Comment search
menu.tools=도구
menu.deobfuscation=난독화 해제
menu.log=로그 뷰어
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=<html>Jadx의 메모리가 부족합니다. 따
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB
#common_dialog.ok=Ok
#common_dialog.cancel=Cancel
#common_dialog.add=Add
#common_dialog.update=Update
#common_dialog.remove=Remove
search_dialog.open=열기
search_dialog.cancel=취소
search_dialog.open_by_name=텍스트 검색 :
@@ -77,6 +84,8 @@ search_dialog.info_label=%3$d 중 %1$d-%2$d 결과 표시
search_dialog.col_node=노드
search_dialog.col_code=코드
search_dialog.regex=정규식
#search_dialog.active_tab=Active tab only
#search_dialog.comments=Comments
search_dialog.resource=리소스
search_dialog.keep_open=열어 두기
search_dialog.tip_searching=검색 중...
@@ -84,6 +93,11 @@ search_dialog.tip_searching=검색 중...
usage_dialog.title=사용 검색
usage_dialog.label=다음의 사용 검색 결과:
#comment_dialog.title.add=Add code comment
#comment_dialog.title.update=Update code comment
#comment_dialog.label=Comment:
#comment_dialog.usage=
log_viewer.title=로그 뷰어
log_viewer.log_level=로그 레벨:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=난독 해제 활성화
msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다.
msg.rename_node_disabled=이 노드의 이름을 바꿀 수 없습니다.
msg.rename_node_failed=%s의 이름을 바꿀 수 없습니다.
#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
popup.line_wrap=줄 바꿈
@@ -168,6 +183,8 @@ popup.select_all=모두 선택
popup.find_usage=사용 찾기
popup.go_to_declaration=선언문으로 이동
popup.exclude=제외
#popup.add_comment=Comment
#popup.search_comment=Search comments
popup.rename=이름 바꾸기
popup.search="%s" 검색
popup.search_global="%s" 전역 검색
@@ -11,6 +11,7 @@ menu.heapUsageBar=显示内存使用栏
menu.navigation=导航
menu.text_search=搜索文本
menu.class_search=搜索类
#menu.comment_search=Comment search
menu.tools=工具
menu.deobfuscation=反混淆
menu.log=日志查看器
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=<html>Jadx 的内存不足。因此,%d 类没
heapUsage.text=JADX 内存使用率:%.2f GB 共 %.2f GB
#common_dialog.ok=Ok
#common_dialog.cancel=Cancel
#common_dialog.add=Add
#common_dialog.update=Update
#common_dialog.remove=Remove
search_dialog.open=转到
search_dialog.cancel=取消
search_dialog.open_by_name=搜索文本:
@@ -77,6 +84,8 @@ search_dialog.info_label=显示了 %3$d 个结果中的第 %1$d 至第 %2$d 个
search_dialog.col_node=节点
search_dialog.col_code=代码
search_dialog.regex=正则表达式
#search_dialog.active_tab=Active tab only
#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=正则表达式
usage_dialog.title=查找
usage_dialog.label=查找用例:
#comment_dialog.title.add=Add code comment
#comment_dialog.title.update=Update code comment
#comment_dialog.label=Comment:
#comment_dialog.usage=
log_viewer.title=日志查看器
log_viewer.log_level=日志等级:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=请启用反混淆以重命名。
msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。
#msg.rename_node_disabled=
#msg.rename_node_failed=
#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=全选
popup.find_usage=查找用例
popup.go_to_declaration=跳到声明
popup.exclude=排除
#popup.add_comment=Comment
#popup.search_comment=Search comments
popup.rename=改名
#popup.search=
#popup.search_global=
Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

@@ -118,6 +118,6 @@ class JumpManagerTest {
}
private JumpPosition makeJumpPos() {
return new JumpPosition(new TextNode(""), 0);
return new JumpPosition(new TextNode(""), 0, 0);
}
}