feat(gui): a tree structure usage search (PR #2498)

* A tree - shaped usage rendering scheme has been added

* A tree - shaped usage rendering scheme has been added - i18n update

---------

Co-authored-by: y <y@a.com>
This commit is contained in:
Little Hour Y
2025-05-21 03:46:07 +08:00
committed by GitHub
parent bee476895c
commit d0351a88ba
13 changed files with 1079 additions and 0 deletions
@@ -83,6 +83,8 @@ public enum ActionModel {
FIND_USAGE(CODE_AREA, "popup.find_usage", "popup.find_usage", null,
Shortcut.keyboard(KeyEvent.VK_X)),
FIND_USAGE_PLUS(CODE_AREA, "popup.usage_dialog_plus", "popup.usage_dialog_plus", null,
Shortcut.keyboard(KeyEvent.VK_C)),
GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", "popup.go_to_declaration", null,
Shortcut.keyboard(KeyEvent.VK_D)),
CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null,
@@ -148,6 +148,7 @@ public final class CodeArea extends AbstractCodeArea {
JNodePopupBuilder popup = new JNodePopupBuilder(this, popupMenu, shortcutsController);
popup.addSeparator();
popup.add(new FindUsageAction(this));
popup.add(new UsageDialogPlusAction(this));
popup.add(new GoToDeclarationAction(this));
popup.add(new CommentAction(this));
popup.add(new CommentSearchAction(this));
@@ -0,0 +1,19 @@
package jadx.gui.ui.codearea;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.action.ActionModel;
import jadx.gui.ui.action.JNodeAction;
import jadx.gui.ui.dialog.UsageDialogPlus;
public final class UsageDialogPlusAction extends JNodeAction {
private static final long serialVersionUID = 4692546569977976384L;
public UsageDialogPlusAction(CodeArea codeArea) {
super(ActionModel.FIND_USAGE_PLUS, codeArea);
}
@Override
public void runAction(JNode node) {
UsageDialogPlus.open(getCodeArea().getMainWindow(), node);
}
}
@@ -0,0 +1,976 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.table.TableCellRenderer;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.utils.CodeUtils;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.visitors.prepare.CollectConstValues;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.NodeLabel;
public class UsageDialogPlus extends CommonSearchDialog {
private static final long serialVersionUID = -5105405789969134107L;
private final List<UsagePanel> usagePanels = new ArrayList<>();
private final JPanel mainPanel;
private final JSplitPane splitPane;
private final CodePanel codePanel;
private static final Logger LOG = LoggerFactory.getLogger(UsageDialogPlus.class);
public static void open(MainWindow mainWindow, JNode node) {
UsageDialogPlus usageDialog = new UsageDialogPlus(mainWindow, node);
mainWindow.addLoadListener(loaded -> {
if (!loaded) {
usageDialog.dispose();
return true;
}
return false;
});
usageDialog.setVisible(true);
// Set the position of the split panel after the window is displayed.
SwingUtilities.invokeLater(() -> {
int width = usageDialog.splitPane.getWidth();
if (width > 0) {
usageDialog.splitPane.setDividerLocation((int) (width * 0.7));
}
});
}
private UsageDialogPlus(MainWindow mainWindow, JNode node) {
super(mainWindow, NLS.str("usage_dialog_plus.title"));
// Initialize the progress panel and warning label
progressPane = new ProgressPanel(mainWindow, false);
warnLabel = new JLabel();
warnLabel.setForeground(Color.RED);
warnLabel.setVisible(false);
mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.X_AXIS));
// Create the code panel
codePanel = new CodePanel(mainWindow);
// Create the split panel, allowing horizontal scrolling
splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setOneTouchExpandable(true);
splitPane.setContinuousLayout(true);
splitPane.setResizeWeight(0.7); // Left 70%
splitPane.setDividerSize(10); // Increase the width of the divider for easier dragging
// Need a wrapper panel to support horizontal scrolling
JScrollPane scrollPane = new JScrollPane(mainPanel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
// Create the first panel but do not initialize it
UsagePanel panel = new UsagePanel(this, node, usagePanels.size());
usagePanels.add(panel);
mainPanel.add(panel);
initUI();
registerInitOnOpen();
loadWindowPos();
}
private void addUsagePanel(JNode node) {
// If there is already a panel, update the panel on the right
if (!usagePanels.isEmpty()) {
int lastIndex = usagePanels.size() - 1;
// If there are multiple panels, remove all panels on the right of the last panel
while (usagePanels.size() > lastIndex + 1) {
UsagePanel panelToRemove = usagePanels.get(usagePanels.size() - 1);
mainPanel.remove(panelToRemove);
usagePanels.remove(usagePanels.size() - 1);
}
}
// Create a new panel and add it
UsagePanel panel = new UsagePanel(this, node, usagePanels.size());
usagePanels.add(panel);
mainPanel.add(panel);
mainPanel.revalidate();
mainPanel.repaint();
// If a new panel is added, scroll to the right
scrollToRight();
// Initialize the new added panel
panel.openInit();
}
private void scrollToRight() {
// Ensure the layout is updated
mainPanel.revalidate();
// Get the scroll panel
JScrollPane scrollPane = (JScrollPane) splitPane.getLeftComponent();
// Use SwingUtilities.invokeLater to ensure the scroll is executed after the UI is updated
SwingUtilities.invokeLater(() -> {
// Get the horizontal scroll bar
JScrollBar horizontalScrollBar = scrollPane.getHorizontalScrollBar();
// Scroll to the right
horizontalScrollBar.setValue(horizontalScrollBar.getMaximum());
});
}
@Override
protected void openInit() {
// Only initialize the first panel, other panels will be initialized automatically when added
if (!usagePanels.isEmpty()) {
usagePanels.get(0).openInit();
}
}
@Override
protected void loadFinished() {
// This method is handled separately in each UsagePanel
}
@Override
protected void loadStart() {
// This method is handled separately in each UsagePanel
}
private void initUI() {
JadxSettings settings = mainWindow.getSettings();
Font codeFont = settings.getFont();
JScrollPane scrollPane = new JScrollPane(mainPanel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
splitPane.setLeftComponent(scrollPane);
splitPane.setRightComponent(codePanel); // Set the right side to the code panel
initCommon();
JPanel buttonPane = initButtonsPanel();
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(splitPane, BorderLayout.CENTER);
contentPanel.add(buttonPane, BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
pack();
setSize(1300, 700);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// Add a window size change listener to ensure the divider position adapts to window size changes
addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentResized(java.awt.event.ComponentEvent e) {
// Keep the left-right ratio
int width = splitPane.getWidth();
if (width > 0) {
int currentDividerLocation = splitPane.getDividerLocation();
double ratio = (double) currentDividerLocation / width;
// If the ratio is too small or too large, reset to a reasonable value
if (ratio < 0.3 || ratio > 0.85) {
splitPane.setDividerLocation((int) (width * 0.7)); // Use pixel values
}
}
}
});
}
// The code panel class is used to display the code of the selected node.
private class CodePanel extends JPanel {
private final RSyntaxTextArea codeArea;
private final JLabel titleLabel;
public CodePanel(MainWindow mainWindow) {
setLayout(new BorderLayout(5, 5));
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
// Set the minimum size to ensure the panel is not completely minimized
setMinimumSize(new Dimension(300, 400));
setPreferredSize(new Dimension(400, 600));
// The title label
titleLabel = new JLabel(NLS.str("usage_dialog_plus.code_view"));
titleLabel.setFont(codeFont);
titleLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 10, 5));
// The code area
codeArea = AbstractCodeArea.getDefaultArea(mainWindow);
codeArea.setEditable(false);
codeArea.setText("// " + NLS.str("usage_dialog_plus.select_node"));
JScrollPane scrollPane = new JScrollPane(codeArea);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
add(titleLabel, BorderLayout.NORTH);
add(scrollPane, BorderLayout.CENTER);
}
public void showCode(JNode node, String codeLine) {
if (node != null) {
titleLabel.setText(NLS.str("usage_dialog_plus.code_for") + " " + node.makeLongString());
codeArea.setSyntaxEditingStyle(node.getSyntaxName());
// Get the complete code
String contextCode = getContextCode(node, codeLine);
codeArea.setText(contextCode);
// Highlight the key line and scroll to that position
scrollToCodeLine(codeArea, codeLine);
// If it is a CodeNode, we can get a more precise position
if (node instanceof CodeNode) {
CodeNode codeNode = (CodeNode) node;
int pos = codeNode.getPos();
if (pos > 0) {
// Try to use the position information to more accurately locate
try {
String text = codeArea.getText();
int lineNum = 0;
int curPos = 0;
// Calculate the line number corresponding to the position
for (int i = 0; i < text.length() && curPos <= pos; i++) {
if (text.charAt(i) == '\n') {
lineNum++;
}
curPos++;
}
if (lineNum > 0) {
// Scroll to the calculated line number
int finalLineNum = lineNum;
SwingUtilities.invokeLater(() -> {
try {
Rectangle lineRect = codeArea
.modelToView(codeArea.getLineStartOffset(finalLineNum));
if (lineRect != null) {
JScrollPane scrollPane = (JScrollPane) codeArea.getParent().getParent();
Rectangle viewRect = scrollPane.getViewport().getViewRect();
int y = lineRect.y - (viewRect.height - lineRect.height) / 2;
if (y < 0) {
y = 0;
}
scrollPane.getViewport().setViewPosition(new Point(0, y));
}
} catch (Exception e) {
// Fall back to using string matching
scrollToCodeLine(codeArea, codeLine);
}
});
}
} catch (Exception e) {
// Fall back to using string matching
scrollToCodeLine(codeArea, codeLine);
}
} else {
// If there is no position information, use string matching
scrollToCodeLine(codeArea, codeLine);
}
} else {
// Not a CodeNode, use string matching
scrollToCodeLine(codeArea, codeLine);
}
} else {
titleLabel.setText(NLS.str("usage_dialog_plus.code_view"));
codeArea.setText("// " + NLS.str("usage_dialog_plus.select_node"));
}
}
private String getContextCode(JNode node, String codeLine) {
// Always try to get the complete code
if (node instanceof CodeNode) {
CodeNode codeNode = (CodeNode) node;
JNode usageJNode = codeNode.getJParent();
if (usageJNode != null) {
// Try to get the complete code of the method or class
String fullCode = getFullNodeCode(usageJNode);
if (fullCode != null && !fullCode.isEmpty()) {
return fullCode;
}
}
}
// If you cannot get more context, at least add some empty lines and comments
return "// Unable to get complete context, only display related lines\n\n" + codeLine;
}
private String getFullNodeCode(JNode node) {
if (node != null) {
// Get the code information of the node
ICodeInfo codeInfo = node.getCodeInfo();
if (codeInfo != null && !codeInfo.equals(ICodeInfo.EMPTY)) {
return codeInfo.getCodeStr();
}
// If it is a class node, try to get the class code
if (node instanceof JClass) {
JClass jClass = (JClass) node;
return jClass.getCodeInfo().getCodeStr();
}
}
return null;
}
private void scrollToCodeLine(RSyntaxTextArea textArea, String lineToHighlight) {
// Try to find and highlight a specific line in the code and scroll to that position
try {
String fullText = textArea.getText();
int lineIndex = fullText.indexOf(lineToHighlight);
if (lineIndex >= 0) {
// Ensure the text area has updated the layout
textArea.revalidate();
// Highlight the code line
textArea.setCaretPosition(lineIndex);
int endIndex = lineIndex + lineToHighlight.length();
textArea.select(lineIndex, endIndex);
textArea.getCaret().setSelectionVisible(true);
// Use SwingUtilities.invokeLater to ensure the scroll is executed after the UI is updated
SwingUtilities.invokeLater(() -> {
try {
// Get the line number
int lineNum = textArea.getLineOfOffset(lineIndex);
// Ensure the line is centered in the view
Rectangle lineRect = textArea.modelToView(textArea.getLineStartOffset(lineNum));
if (lineRect != null) {
// Calculate the center point of the view
JScrollPane scrollPane = (JScrollPane) textArea.getParent().getParent();
Rectangle viewRect = scrollPane.getViewport().getViewRect();
int y = lineRect.y - (viewRect.height - lineRect.height) / 2;
if (y < 0) {
y = 0;
}
// Scroll to the calculated position
scrollPane.getViewport().setViewPosition(new Point(0, y));
}
} catch (Exception e) {
LOG.debug("Error scrolling to line: {}", e.getMessage());
}
});
} else {
LOG.debug("Could not find line to highlight: {}", lineToHighlight);
}
} catch (Exception e) {
LOG.debug("Error highlighting line: {}", e.getMessage());
}
}
}
private class UsagePanel extends JPanel {
private final UsageDialogPlus parentDialog;
private final JNode node;
private final int index;
private List<CodeNode> usageList;
private ResultsModel resultsModel;
private JTable resultsTable;
private JLabel resultsInfoLabel;
private JLabel progressInfoLabel;
private ProgressPanel localProgressPanel; // Add local progress panel
// Unify the height of all panels.
private static final int PANEL_HEIGHT = 650;
private static final int PANEL_WIDTH = 450; // Increase width to display more node content
public UsagePanel(UsageDialogPlus parentDialog, JNode node, int index) {
this.parentDialog = parentDialog;
this.node = node;
this.index = index;
setLayout(new BorderLayout(5, 5));
setBorder(BorderFactory.createTitledBorder("Usage " + (index + 1)));
// Set the unified panel size
setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
setMinimumSize(new Dimension(250, PANEL_HEIGHT)); // Set the minimum width to 250
initPanelUI();
}
private void initPanelUI() {
// Initialize the local progress panel
localProgressPanel = new ProgressPanel(mainWindow, false);
// Search panel - use a text area instead of a label to support automatic line wrapping
JPanel searchPane = new JPanel(new BorderLayout());
JLabel lbl = new JLabel(NLS.str("usage_dialog.label"));
lbl.setFont(codeFont);
// Use NodeLabel.longName to display node information, including icons
JLabel nodeLabel = NodeLabel.longName(node);
nodeLabel.setFont(codeFont);
lbl.setLabelFor(nodeLabel);
JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
labelPanel.add(lbl);
searchPane.add(labelPanel, BorderLayout.NORTH);
searchPane.add(nodeLabel, BorderLayout.CENTER);
searchPane.setBorder(BorderFactory.createEmptyBorder(15, 10, 15, 10)); // Increase the top and bottom margins
// Result table - initialized exactly as CommonSearchDialog.
EnhancedCellRenderer renderer = new EnhancedCellRenderer();
resultsModel = new ResultsModel();
resultsTable = new JTable(resultsModel);
resultsTable.setRowHeight(renderer.getMaxRowHeight());
resultsTable.setShowHorizontalLines(false);
resultsTable.setDragEnabled(false);
resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
resultsTable.setColumnSelectionAllowed(false);
resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // Set to not automatically adjust column widths
resultsTable.setAutoscrolls(true); // Enable automatic scrolling
resultsTable.setDefaultRenderer(Object.class, renderer);
// Remove the second column (code column)
if (resultsTable.getColumnCount() > 1) {
resultsTable.removeColumn(resultsTable.getColumnModel().getColumn(1));
}
// Add a selection listener to display code when a node is selected
resultsTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
int selectedRow = resultsTable.getSelectedRow();
if (selectedRow >= 0 && selectedRow < resultsModel.getRowCount()) {
JNode selectedNode = (JNode) resultsModel.getValueAt(selectedRow, 0);
if (selectedNode instanceof CodeNode) {
CodeNode codeNode = (CodeNode) selectedNode;
codePanel.showCode(codeNode, codeNode.makeDescString());
} else {
codePanel.showCode(selectedNode, selectedNode.makeDescString());
}
}
}
});
resultsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int row = resultsTable.rowAtPoint(e.getPoint());
if (row >= 0) {
JNode selectedNode = (JNode) resultsModel.getValueAt(row, 0);
if (selectedNode != null) {
// Get the actual node clicked
JNode nodeToUse = selectedNode;
// If it is a CodeNode, we need to get the actual node it references
if (selectedNode instanceof CodeNode) {
CodeNode codeNode = (CodeNode) selectedNode;
// Use JavaNode to determine the correct node type
JavaNode javaNode = codeNode.getJavaNode();
// Use NodeCache to convert JavaNode to JNode
JNodeCache nodeCache = getNodeCache();
nodeToUse = nodeCache.makeFrom(javaNode);
}
// If the current panel is not the last panel, update the right panel
// Otherwise, add a new panel
if (index < usagePanels.size() - 1) {
// Update the right panel
updateRightPanel(nodeToUse);
} else {
// Add a new panel
parentDialog.addUsagePanel(nodeToUse);
}
// Whether updating or adding, scroll to the right
parentDialog.scrollToRight();
}
}
} else if (e.getButton() == MouseEvent.BUTTON3) {
int row = resultsTable.rowAtPoint(e.getPoint());
if (row >= 0) {
resultsTable.setRowSelectionInterval(row, row);
JNode selectedNode = (JNode) resultsModel.getValueAt(row, 0);
if (selectedNode != null) {
showPopupMenu(e, selectedNode);
}
}
}
}
});
JScrollPane scrollPane = new JScrollPane(resultsTable);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
// Set the preferred scroll size of the table to support displaying longer content
resultsTable.setPreferredScrollableViewportSize(new Dimension(PANEL_WIDTH - 50, 500));
// Ensure the table can scroll to display long content
resultsTable.getTableHeader().setResizingAllowed(true);
resultsTable.getTableHeader().setReorderingAllowed(false);
// Status panel
JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
resultsInfoLabel = new JLabel("");
progressInfoLabel = new JLabel("");
statusPanel.add(resultsInfoLabel);
statusPanel.add(Box.createRigidArea(new Dimension(10, 0)));
statusPanel.add(progressInfoLabel);
statusPanel.add(Box.createRigidArea(new Dimension(10, 0)));
statusPanel.add(localProgressPanel); // Use the local progress panel
// Main panel layout
add(searchPane, BorderLayout.NORTH);
add(scrollPane, BorderLayout.CENTER);
add(statusPanel, BorderLayout.SOUTH);
}
private void showPopupMenu(MouseEvent e, JNode node) {
JPopupMenu popup = new JPopupMenu();
JMenuItem jumpToItem = new JMenuItem(NLS.str("usage_dialog_plus.jump_to"));
jumpToItem.addActionListener(evt -> {
parentDialog.openItem(node);
});
JMenuItem copyPathItem = new JMenuItem(NLS.str("usage_dialog_plus.copy_path"));
copyPathItem.addActionListener(evt -> {
copyUsagePath(node);
});
popup.add(jumpToItem);
popup.add(copyPathItem);
popup.show(e.getComponent(), e.getX(), e.getY());
}
private void copyUsagePath(JNode node) {
if (node != null) {
// Build the complete path from the leftmost node to the current node
StringBuilder pathBuilder = new StringBuilder();
// Get the node path from all panels from left to right
List<JNode> nodePath = new ArrayList<>();
// Add the nodes from all panels from left to right
for (int i = 0; i <= index; i++) {
UsagePanel panel = usagePanels.get(i);
nodePath.add(panel.node);
}
// Add the currently selected node (if it is different from the current panel node)
if (!node.equals(nodePath.get(nodePath.size() - 1))) {
nodePath.add(node);
}
Collections.reverse(nodePath);
// Build the path string in the required format: the leftmost node is at the top, and each level
// adds indentation and an arrow
// For example: a\n -> b\n -> c\n -> d
// Build the path string
for (int i = 0; i < nodePath.size(); i++) {
if (i > 0) {
pathBuilder.append("\n");
// Add indentation (add one space for each level)
for (int j = 0; j < i; j++) {
pathBuilder.append(" ");
}
pathBuilder.append("-> ");
}
// Add node information
JNode currentNode = nodePath.get(i);
pathBuilder.append(currentNode.getJavaNode().getCodeNodeRef().toString());
}
// Copy to clipboard
UiUtils.copyToClipboard(pathBuilder.toString());
}
}
public void openInit() {
localProgressPanel.setIndeterminate(true);
localProgressPanel.setVisible(true);
prepareUsageData();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"),
this::collectUsageData,
(status) -> {
if (status == TaskStatus.CANCEL_BY_MEMORY) {
mainWindow.showHeapUsageBar();
UiUtils.errorMessage(UsageDialogPlus.this, NLS.str("message.memoryLow"));
}
localProgressPanel.setVisible(false);
onLoadFinished();
});
}
private void prepareUsageData() {
if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = CollectConstValues.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// run full decompilation to prepare for full code scan
mainWindow.requestFullDecompilation();
}
}
}
private void collectUsageData() {
usageList = new ArrayList<>();
buildUsageQuery().forEach(
(searchNode, useNodes) -> useNodes.stream()
.map(JavaNode::getTopParentClass)
.distinct()
.forEach(u -> processUsage(searchNode, u)));
}
private Map<JavaNode, List<? extends JavaNode>> buildUsageQuery() {
Map<JavaNode, List<? extends JavaNode>> map = new HashMap<>();
if (node instanceof JMethod) {
JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
for (JavaMethod mth : getMethodWithOverrides(javaMethod)) {
map.put(mth, mth.getUseIn());
}
return map;
}
if (node instanceof JClass) {
JavaClass javaCls = ((JClass) node).getCls();
map.put(javaCls, javaCls.getUseIn());
// add constructors usage into class usage
for (JavaMethod javaMth : javaCls.getMethods()) {
if (javaMth.isConstructor()) {
map.put(javaMth, javaMth.getUseIn());
}
}
return map;
}
if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = CollectConstValues.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// search all classes to collect usage of replaced constants
map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses());
return map;
}
}
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
return map;
}
private List<JavaMethod> getMethodWithOverrides(JavaMethod javaMethod) {
List<JavaMethod> relatedMethods = javaMethod.getOverrideRelatedMethods();
if (!relatedMethods.isEmpty()) {
return relatedMethods;
}
return Collections.singletonList(javaMethod);
}
private void processUsage(JavaNode searchNode, JavaClass topUseClass) {
ICodeInfo codeInfo = topUseClass.getCodeInfo();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
if (usePositions.isEmpty()) {
return;
}
String code = codeInfo.getCodeStr();
JadxWrapper wrapper = mainWindow.getWrapper();
for (int pos : usePositions) {
String line = CodeUtils.getLineForPos(code, pos);
if (line.startsWith("import ")) {
continue;
}
JNodeCache nodeCache = getNodeCache();
JavaNode enclosingNode = wrapper.getEnclosingNode(codeInfo, pos);
JClass rootJCls = nodeCache.makeFrom(topUseClass);
JNode usageJNode = enclosingNode == null ? rootJCls : nodeCache.makeFrom(enclosingNode);
// Create CodeNode and add to list
CodeNode codeNode = new CodeNode(rootJCls, usageJNode, line.trim(), pos);
usageList.add(codeNode);
}
}
private void onLoadFinished() {
resultsTable.setEnabled(true);
resultsModel.clear();
Collections.sort(usageList);
resultsModel.addAll(usageList);
updateHighlightContext(node.getName(), true, false, true);
updateTableUI();
updateProgressLabel(true);
// If there are results, select the first one to display code
if (!usageList.isEmpty()) {
resultsTable.setRowSelectionInterval(0, 0);
CodeNode firstNode = usageList.get(0);
codePanel.showCode(firstNode, firstNode.makeDescString());
}
}
private void updateTableUI() {
if (resultsTable != null) {
// Set a sufficiently large column width to display complete content
int columnCount = resultsTable.getColumnCount();
int width = Math.max(resultsTable.getParent().getWidth(), PANEL_WIDTH);
// Use a larger width value to display more content
int colWidth = width + 500; // Use a sufficiently large width to ensure content can be fully displayed
resultsTable.getColumnModel().getColumn(0).setPreferredWidth(colWidth);
for (int col = 1; col < columnCount; col++) {
resultsTable.getColumnModel().getColumn(col).setPreferredWidth(width);
}
// Update the table UI
resultsTable.updateUI();
}
}
private void updateProgressLabel(boolean complete) {
if (complete) {
progressInfoLabel.setText(NLS.str("usage_dialog_plus.search_complete"));
} else {
progressInfoLabel.setText(NLS.str("search_dialog.tip_searching"));
}
// Update the results information label
resultsInfoLabel.setText(NLS.str("search_dialog.results_complete", usageList.size()));
}
// Update the right panel method
private void updateRightPanel(JNode selectedNode) {
// Get the right panel index
int rightPanelIndex = index + 1;
// If the right panel does not exist, add a new panel
if (rightPanelIndex >= usagePanels.size()) {
parentDialog.addUsagePanel(selectedNode);
return;
}
// Get the right panel
UsagePanel rightPanel = usagePanels.get(rightPanelIndex);
// Create a new panel to replace the right panel
UsagePanel newPanel = new UsagePanel(parentDialog, selectedNode, rightPanelIndex);
// Replace the panel
mainPanel.remove(rightPanel);
mainPanel.add(newPanel, rightPanelIndex);
usagePanels.set(rightPanelIndex, newPanel);
// Remove all panels to the right
while (usagePanels.size() > rightPanelIndex + 1) {
UsagePanel panelToRemove = usagePanels.get(usagePanels.size() - 1);
mainPanel.remove(panelToRemove);
usagePanels.remove(usagePanels.size() - 1);
}
// Update UI
mainPanel.revalidate();
mainPanel.repaint();
// Initialize the new panel
newPanel.openInit();
// Scroll to the right
parentDialog.scrollToRight();
}
}
@NotNull
@Override
protected JPanel initButtonsPanel() {
progressPane = new ProgressPanel(mainWindow, false);
JButton cancelButton = new JButton(NLS.str("search_dialog.cancel"));
cancelButton.addActionListener(event -> dispose());
JButton openBtn = new JButton(NLS.str("search_dialog.open"));
openBtn.addActionListener(event -> openSelectedItem());
getRootPane().setDefaultButton(openBtn);
// Delete the copy all button
JCheckBox cbKeepOpen = new JCheckBox(NLS.str("search_dialog.keep_open"));
cbKeepOpen.setSelected(mainWindow.getSettings().getKeepCommonDialogOpen());
cbKeepOpen.addActionListener(e -> {
mainWindow.getSettings().setKeepCommonDialogOpen(cbKeepOpen.isSelected());
mainWindow.getSettings().sync();
});
cbKeepOpen.setAlignmentY(Component.CENTER_ALIGNMENT);
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.add(cbKeepOpen);
buttonPane.add(Box.createRigidArea(new Dimension(15, 0)));
buttonPane.add(progressPane);
buttonPane.add(Box.createRigidArea(new Dimension(5, 0)));
buttonPane.add(Box.createHorizontalGlue());
// Do not add the copy all button
buttonPane.add(openBtn);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelButton);
return buttonPane;
}
@Override
protected void openSelectedItem() {
// Get the currently selected node
JNode node = getSelectedNode();
if (node == null) {
return;
}
openItem(node);
}
@Nullable
private JNode getSelectedNode() {
try {
// Iterate through all panels to find the selected node
for (UsagePanel panel : usagePanels) {
int selectedRow = panel.resultsTable.getSelectedRow();
if (selectedRow != -1 && selectedRow < panel.resultsModel.getRowCount()) {
return (JNode) panel.resultsModel.getValueAt(selectedRow, 0);
}
}
return null;
} catch (Exception e) {
LOG.error("Failed to get results table selected object", e);
return null;
}
}
// Custom table cell renderer, fully implemented by referring to the ResultsTableCellRenderer in
// CommonSearchDialog.
private class EnhancedCellRenderer implements TableCellRenderer {
private final NodeLabel label;
private final RSyntaxTextArea codeArea;
private final NodeLabel emptyLabel;
private final Color codeSelectedColor;
private final Color codeBackground;
public EnhancedCellRenderer() {
codeArea = AbstractCodeArea.getDefaultArea(mainWindow);
codeArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
codeArea.setRows(1);
codeBackground = codeArea.getBackground();
codeSelectedColor = codeArea.getSelectionColor();
label = new NodeLabel();
label.setOpaque(true);
label.setFont(codeArea.getFont());
label.setHorizontalAlignment(SwingConstants.LEFT);
emptyLabel = new NodeLabel();
emptyLabel.setOpaque(true);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, boolean hasFocus,
int row,
int column) {
if (obj == null || table == null) {
return emptyLabel;
}
Component comp = makeCell((JNode) obj, column);
updateSelection(table, comp, column, isSelected);
return comp;
}
private void updateSelection(JTable table, Component comp, int column, boolean isSelected) {
if (column == 1) {
if (isSelected) {
comp.setBackground(codeSelectedColor);
} else {
comp.setBackground(codeBackground);
}
} else {
if (isSelected) {
comp.setBackground(table.getSelectionBackground());
comp.setForeground(table.getSelectionForeground());
} else {
comp.setBackground(table.getBackground());
comp.setForeground(table.getForeground());
}
}
}
private Component makeCell(JNode node, int column) {
if (column == 0) {
label.disableHtml(node.disableHtml());
// Use the complete node information, not truncated
String nodeText = node.makeLongStringHtml();
label.setText(nodeText);
label.setToolTipText(node.getTooltip());
label.setIcon(node.getIcon());
return label;
}
if (!node.hasDescString()) {
return emptyLabel;
}
codeArea.setSyntaxEditingStyle(node.getSyntaxName());
String descStr = node.makeDescString();
codeArea.setText(descStr);
codeArea.setColumns(descStr.length() + 1);
return codeArea;
}
public int getMaxRowHeight() {
label.setText("Text");
codeArea.setText("Text");
return Math.max(getCompHeight(label), getCompHeight(codeArea));
}
private int getCompHeight(Component comp) {
return Math.max(comp.getHeight(), comp.getPreferredSize().height);
}
}
}
@@ -184,6 +184,14 @@ search_dialog.copy=alles kopieren
usage_dialog.title=Verwendungssuche
usage_dialog.label=Verwendungen von:
#usage_dialog_plus.title=Verwendungssuche
usage_dialog_plus.jump_to=Zur aktuellen Position springen
usage_dialog_plus.copy_path=Verwendungspfad kopieren
usage_dialog_plus.search_complete=Suche abgeschlossen
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
comment_dialog.title.add=Code-Kommentar hinzufügen
comment_dialog.title.update=Code-Kommentar aktualisieren
comment_dialog.label=Kommentar:
@@ -363,6 +371,7 @@ popup.add_scripts=Skripte hinzufügen
popup.new_script=Neues Skript
popup.json_prettify=JSON-Verschönerung
popup.export=Exportieren
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=copy all
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
usage_dialog_plus.title=Usage tree search
usage_dialog_plus.jump_to=Jump to current location
usage_dialog_plus.copy_path=Copy usage tree path
usage_dialog_plus.search_complete=Search completed
usage_dialog_plus.code_view=code view
usage_dialog_plus.select_node=select node
usage_dialog_plus.code_for=code for
comment_dialog.title.add=Add code comment
comment_dialog.title.update=Update code comment
comment_dialog.label=Comment:
@@ -363,6 +371,7 @@ popup.add_scripts=Add scripts
popup.new_script=New script
popup.json_prettify=JSON Prettify
popup.export=Export
popup.usage_dialog_plus=Usage Tree Search
popup.copy_as=Copy as...
popup.copy_as_hex=Copy as HEX
popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=copiar todo
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
#usage_dialog_plus.title=Usage tree search
#usage_dialog_plus.jump_to=Jump to current location
#usage_dialog_plus.copy_path=Copy usage tree path
#usage_dialog_plus.search_complete=Search completed
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
#comment_dialog.title.add=Add code comment
#comment_dialog.title.update=Update code comment
#comment_dialog.label=Comment:
@@ -363,6 +371,7 @@ popup.rename=Renombrar
#popup.new_script=New script
#popup.json_prettify=JSON Prettify
#popup.export=Export
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=salin semua
usage_dialog.title=Pencarian penggunaan
usage_dialog.label=Penggunaan untuk:
#usage_dialog_plus.title=Usage tree search
#usage_dialog_plus.jump_to=Jump to current location
#usage_dialog_plus.copy_path=Copy usage tree path
#usage_dialog_plus.search_complete=Search completed
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
comment_dialog.title.add=Tambahkan komentar kode
comment_dialog.title.update=Perbarui komentar kode
comment_dialog.label=Komentar:
@@ -363,6 +371,7 @@ popup.add_scripts=Tambahkan skrip
popup.new_script=Skrip baru
popup.json_prettify=JSON Prettify
#popup.export=Export
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=모두 복사
usage_dialog.title=사용 검색
usage_dialog.label=다음의 사용 검색 결과:
#usage_dialog_plus.title=Usage tree search
#usage_dialog_plus.jump_to=Jump to current location
#usage_dialog_plus.copy_path=Copy usage tree path
#usage_dialog_plus.search_complete=Search completed
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
comment_dialog.title.add=주석 추가
comment_dialog.title.update=주석 업데이트
comment_dialog.label=주석:
@@ -363,6 +371,7 @@ popup.search_global="%s" 전역 검색
#popup.new_script=New script
#popup.json_prettify=JSON Prettify
#popup.export=Export
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=skopiuj wszystko
usage_dialog.title=Busca por utilização
usage_dialog.label=Usado por:
#usage_dialog_plus.title=Usage tree search
#usage_dialog_plus.jump_to=Jump to current location
#usage_dialog_plus.copy_path=Copy usage tree path
#usage_dialog_plus.search_complete=Search completed
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
comment_dialog.title.add=Adicionar comentário ao código
comment_dialog.title.update=Atualizar comentário do código
comment_dialog.label=Comentário:
@@ -363,6 +371,7 @@ popup.search_global=Busca global "%s"
#popup.new_script=New script
#popup.json_prettify=JSON Prettify
#popup.export=Export
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=скопировать все
usage_dialog.title=Поиск использований
usage_dialog.label=Использования:
#usage_dialog_plus.title=Usage tree search
#usage_dialog_plus.jump_to=Jump to current location
#usage_dialog_plus.copy_path=Copy usage tree path
#usage_dialog_plus.search_complete=Search completed
#usage_dialog_plus.code_view=code view
#usage_dialog_plus.select_node=select node
#usage_dialog_plus.code_for=code for
comment_dialog.title.add=Добавить комментарий
comment_dialog.title.update=Обновить
comment_dialog.label=Комментарий:
@@ -363,6 +371,7 @@ popup.add_scripts=Добавить скрипты
popup.new_script=Новый скрипт
popup.json_prettify=Форматировать JSON
#popup.export=Export
#popup.usage_dialog_plus=Usage Tree Search
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=复制全部
usage_dialog.title=查找
usage_dialog.label=查找用例:
usage_dialog_plus.title=树状用例查找
usage_dialog_plus.jump_to=跳转到当前位置
usage_dialog_plus.copy_path=复制用例路径
usage_dialog_plus.search_complete=搜索完成
usage_dialog_plus.code_view=代码预览
usage_dialog_plus.select_node=选择节点
usage_dialog_plus.code_for=代码信息
comment_dialog.title.add=添加代码注释
comment_dialog.title.update=更新代码注释
comment_dialog.label=注释:
@@ -363,6 +371,7 @@ popup.add_scripts=添加脚本
popup.new_script=新建脚本
popup.json_prettify=JSON 格式化
popup.export=导出
popup.usage_dialog_plus=树状用例查找
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String
@@ -184,6 +184,14 @@ search_dialog.copy=複製全部
usage_dialog.title=使用情況搜尋
usage_dialog.label=使用情況:
usage_dialog_plus.title=使用情況搜尋
usage_dialog_plus.jump_to=前往目前位置
usage_dialog_plus.copy_path=複製使用情況路徑
usage_dialog_plus.search_complete=搜尋完成
usage_dialog_plus.code_view=程式碼預覽
usage_dialog_plus.select_node=選擇節點
usage_dialog_plus.code_for=程式碼資訊
comment_dialog.title.add=新增程式碼註解
comment_dialog.title.update=更新程式碼註解
comment_dialog.label=註解:
@@ -363,6 +371,7 @@ popup.add_scripts=加入腳本
popup.new_script=新增腳本
popup.json_prettify=JSON 格式化
popup.export=匯出
popup.usage_dialog_plus=使用情況搜尋
#popup.copy_as=Copy as...
#popup.copy_as_hex=Copy as HEX
#popup.copy_as_string=Copy as String