diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java index 8c792d8e0..c099268b9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -12,11 +12,8 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; +import java.util.*; import java.util.List; -import java.util.Map; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; @@ -30,7 +27,6 @@ import jadx.gui.jobs.BackgroundJob; import jadx.gui.jobs.BackgroundWorker; import jadx.gui.jobs.DecompileJob; import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.TextNode; import jadx.gui.utils.CacheObject; import jadx.gui.utils.NLS; import jadx.gui.utils.Position; @@ -41,7 +37,7 @@ public abstract class CommonSearchDialog extends JDialog { private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class); private static final long serialVersionUID = 8939332306115370276L; - public static final int MAX_RESULTS_COUNT = 100; + public static final int RESULTS_PER_PAGE = 100; protected final transient TabbedPane tabbedPane; protected final transient CacheObject cache; @@ -50,10 +46,12 @@ public abstract class CommonSearchDialog extends JDialog { protected ResultsModel resultsModel; protected ResultsTable resultsTable; + protected JLabel resultsInfoLabel; protected JLabel warnLabel; protected ProgressPanel progressPane; protected String highlightText; + protected boolean highlightTextCaseInsensitive = false; public CommonSearchDialog(MainWindow mainWindow) { super(mainWindow); @@ -93,6 +91,11 @@ public abstract class CommonSearchDialog extends JDialog { }); } + protected synchronized void performSearch() { + resultsTable.updateTable(); + updateProgressLabel(); + } + protected void openSelectedItem() { int selectedId = resultsTable.getSelectedRow(); if (selectedId == -1) { @@ -140,6 +143,7 @@ public abstract class CommonSearchDialog extends JDialog { protected JPanel initResultsTable() { ResultsTableCellRenderer renderer = new ResultsTableCellRenderer(); resultsModel = new ResultsModel(renderer); + resultsModel.addTableModelListener((e) -> updateProgressLabel()); resultsTable = new ResultsTable(resultsModel); resultsTable.setShowHorizontalLines(false); resultsTable.setDragEnabled(false); @@ -148,12 +152,7 @@ public abstract class CommonSearchDialog extends JDialog { resultsTable.setColumnSelectionAllowed(false); resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); resultsTable.setAutoscrolls(false); - - Enumeration columns = resultsTable.getColumnModel().getColumns(); - while (columns.hasMoreElements()) { - TableColumn column = columns.nextElement(); - column.setCellRenderer(renderer); - } + resultsTable.setDefaultRenderer(Object.class, renderer); resultsTable.addMouseListener(new MouseAdapter() { @Override @@ -182,10 +181,43 @@ public abstract class CommonSearchDialog extends JDialog { resultsPanel.add(new JScrollPane(resultsTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED)); + + JPanel paginationPanel = new JPanel(); + paginationPanel.setAlignmentX( Component.LEFT_ALIGNMENT ); + paginationPanel.setLayout(new BoxLayout(paginationPanel, BoxLayout.X_AXIS)); + resultsInfoLabel = new JLabel(""); + + JButton nextPageButton = new JButton("->"); + nextPageButton.setToolTipText(NLS.str("search_dialog.next_page")); + nextPageButton.addActionListener((e) -> { + resultsModel.nextPage(); + resultsTable.updateTable(); + resultsTable.scrollRectToVisible(new Rectangle(0,0,1,1)); + }); + + JButton prevPageButton = new JButton("<-"); + prevPageButton.setToolTipText(NLS.str("search_dialog.prev_page")); + prevPageButton.addActionListener((e) -> { + resultsModel.prevPage(); + resultsTable.updateTable(); + resultsTable.scrollRectToVisible(new Rectangle(0,0,1,1)); + }); + + paginationPanel.add(prevPageButton); + paginationPanel.add(nextPageButton); + paginationPanel.add(resultsInfoLabel); + + resultsPanel.add(paginationPanel); resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); return resultsPanel; } + protected void updateProgressLabel() { + String statusText = String.format(NLS.str("search_dialog.info_label"), resultsModel.getDisplayedResultsStart(), + resultsModel.getDisplayedResultsEnd(), resultsModel.getResultCount()); + resultsInfoLabel.setText(statusText); + } + protected static class ResultsTable extends JTable { private static final long serialVersionUID = 3901184054736618969L; @@ -238,23 +270,18 @@ public abstract class CommonSearchDialog extends JDialog { private static final long serialVersionUID = -7821286846923903208L; private static final String[] COLUMN_NAMES = {"Node", "Code"}; - private final transient List rows = new ArrayList<>(); + private final transient ArrayList rows = new ArrayList<>(); private final transient ResultsTableCellRenderer renderer; private transient boolean addDescColumn; + private transient int start = 0; public ResultsModel(ResultsTableCellRenderer renderer) { this.renderer = renderer; } - protected void addAll(Iterable nodes) { + protected void addAll(Collection nodes) { + rows.ensureCapacity(rows.size() + nodes.size()); for (JNode node : nodes) { - int size = getRowCount(); - if (size >= MAX_RESULTS_COUNT) { - if (size == MAX_RESULTS_COUNT) { - add(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")")); - } - return; - } add(node); } } @@ -267,6 +294,7 @@ public abstract class CommonSearchDialog extends JDialog { } public void clear() { + start = 0; addDescColumn = false; rows.clear(); renderer.clear(); @@ -276,9 +304,39 @@ public abstract class CommonSearchDialog extends JDialog { return addDescColumn; } + public int getResultCount() { + return rows.size(); + } + + public int getDisplayedResultsStart() { + if (rows.size() == 0) + return 0; + return start + 1; + } + + public int getDisplayedResultsEnd() { + return Math.min(rows.size(), start + RESULTS_PER_PAGE); + } + + public void nextPage() { + if (start + RESULTS_PER_PAGE < rows.size()) { + renderer.clear(); + start += RESULTS_PER_PAGE; + fireTableStructureChanged(); + } + } + + public void prevPage() { + if (start - RESULTS_PER_PAGE >= 0) { + renderer.clear(); + start -= RESULTS_PER_PAGE; + fireTableStructureChanged(); + } + } + @Override public int getRowCount() { - return rows.size(); + return rows.size() - start; } @Override @@ -293,7 +351,7 @@ public abstract class CommonSearchDialog extends JDialog { @Override public Object getValueAt(int rowIndex, int columnIndex) { - return rows.get(rowIndex); + return rows.get(rowIndex + start); } } @@ -315,7 +373,7 @@ public abstract class CommonSearchDialog extends JDialog { @Override public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, - boolean hasFocus, int row, int column) { + boolean hasFocus, int row, int column) { int id = row << 2 | column; Component comp = componentCache.get(id); if (comp == null) { @@ -359,7 +417,7 @@ public abstract class CommonSearchDialog extends JDialog { textArea.setColumns(textArea.getText().length()); if (highlightText != null) { SearchContext searchContext = new SearchContext(highlightText); - searchContext.setMatchCase(true); + searchContext.setMatchCase(!highlightTextCaseInsensitive); searchContext.setMarkAll(true); SearchEngine.markAll(textArea, searchContext); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index 4f89b91fe..85c576ad1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -31,6 +31,7 @@ public class SearchDialog extends CommonSearchDialog { private Set options = EnumSet.allOf(SearchOptions.class); private JTextField searchField; + private JCheckBox caseChBox; public SearchDialog(MainWindow mainWindow, Set options) { super(mainWindow); @@ -52,33 +53,37 @@ public class SearchDialog extends CommonSearchDialog { searchField.requestFocus(); } - private synchronized void performSearch() { + @Override + protected synchronized void performSearch() { resultsModel.clear(); String text = searchField.getText(); if (text == null || text.isEmpty() || options.isEmpty()) { - resultsTable.updateTable(); return; } - cache.setLastSearch(text); - TextSearchIndex index = cache.getTextIndex(); - if (index == null) { - resultsTable.updateTable(); - return; + try { + cache.setLastSearch(text); + TextSearchIndex index = cache.getTextIndex(); + if (index == null) { + return; + } + boolean caseInsensitive = caseChBox.isSelected(); + if (options.contains(SearchOptions.CLASS)) { + resultsModel.addAll(index.searchClsName(text, caseInsensitive)); + } + if (options.contains(SearchOptions.METHOD)) { + resultsModel.addAll(index.searchMthName(text, caseInsensitive)); + } + if (options.contains(SearchOptions.FIELD)) { + resultsModel.addAll(index.searchFldName(text, caseInsensitive)); + } + if (options.contains(SearchOptions.CODE)) { + resultsModel.addAll(index.searchCode(text, caseInsensitive)); + } + highlightText = text; + highlightTextCaseInsensitive = caseInsensitive; + } finally { + super.performSearch(); } - if (options.contains(SearchOptions.CLASS)) { - resultsModel.addAll(index.searchClsName(text)); - } - if (options.contains(SearchOptions.METHOD)) { - resultsModel.addAll(index.searchMthName(text)); - } - if (options.contains(SearchOptions.FIELD)) { - resultsModel.addAll(index.searchFldName(text)); - } - if (options.contains(SearchOptions.CODE)) { - resultsModel.addAll(index.searchCode(text)); - } - highlightText = text; - resultsTable.updateTable(); } private class SearchFieldListener implements DocumentListener, ActionListener { @@ -89,7 +94,8 @@ public class SearchDialog extends CommonSearchDialog { if (timer != null) { timer.restart(); } else { - timer = new Timer(300, this); + timer = new Timer(400, this); + timer.setRepeats(false); timer.start(); } } @@ -114,24 +120,38 @@ public class SearchDialog extends CommonSearchDialog { private void initUI() { JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name")); - searchField = new JTextField(); searchField.setAlignmentX(LEFT_ALIGNMENT); searchField.getDocument().addDocumentListener(new SearchFieldListener()); new TextStandardActions(searchField); + caseChBox = new JCheckBox(NLS.str("search_dialog.ignorecase")); + caseChBox.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + performSearch(); + } + }); + 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 searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in"))); + searchInPanel.add(clsChBox); + searchInPanel.add(mthChBox); + searchInPanel.add(fldChBox); + searchInPanel.add(codeChBox); + JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT)); - searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in"))); - searchOptions.add(clsChBox); - searchOptions.add(mthChBox); - searchOptions.add(fldChBox); - searchOptions.add(codeChBox); - searchOptions.setAlignmentX(LEFT_ALIGNMENT); + searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.options"))); + searchOptions.add(caseChBox); + + Box box = Box.createHorizontalBox(); + box.setAlignmentX(LEFT_ALIGNMENT); + box.add(searchInPanel); + box.add(searchOptions); JPanel searchPane = new JPanel(); searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS)); @@ -140,7 +160,7 @@ public class SearchDialog extends CommonSearchDialog { searchPane.add(Box.createRigidArea(new Dimension(0, 5))); searchPane.add(searchField); searchPane.add(Box.createRigidArea(new Dimension(0, 5))); - searchPane.add(searchOptions); + searchPane.add(box); searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); initCommon(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java index 87451567a..49accb725 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java @@ -37,7 +37,8 @@ public class UsageDialog extends CommonSearchDialog { // no op } - private synchronized void performSearch() { + @Override + protected synchronized void performSearch() { resultsModel.clear(); CodeUsageInfo usageInfo = cache.getUsageInfo(); @@ -47,7 +48,7 @@ public class UsageDialog extends CommonSearchDialog { resultsModel.addAll(usageInfo.getUsageList(node)); // TODO: highlight only needed node usage highlightText = null; - resultsTable.updateTable(); + super.performSearch(); } private void initUI() { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java index ff5e08b65..c4e7c1569 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -109,4 +109,11 @@ public class Utils { private static String format(long mem) { return Long.toString((long) (mem / 1024. / 1024.)) + "MB"; } + + /** + * Adapt character case for case insensitive searches + */ + public static char caseChar(char ch, boolean toLower) { + return toLower ? Character.toLowerCase(ch) : ch; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java index 4ed5851d2..f71ca1747 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java @@ -7,7 +7,7 @@ import java.util.List; public class CodeIndex implements SearchIndex { private final List keys = new ArrayList<>(); - private final List values = new ArrayList<>(); + private final List values = new ArrayList(); @Override public void put(String str, T value) { @@ -29,15 +29,18 @@ public class CodeIndex implements SearchIndex { } @Override - public List getValuesForKeysContaining(String str) { + public List getValuesForKeysContaining(String str, boolean caseInsensitive) { int size = size(); if (size == 0) { return Collections.emptyList(); } + if (caseInsensitive) { + str = str.toLowerCase(); + } List results = new ArrayList<>(); for (int i = 0; i < size; i++) { StringRef key = keys.get(i); - if (key.indexOf(str) != -1) { + if (key.indexOf(str, caseInsensitive) != -1) { results.add(values.get(i)); } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java index d381b0a4c..9b3fed0b5 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java @@ -10,7 +10,7 @@ public interface SearchIndex { boolean isStringRefSupported(); - List getValuesForKeysContaining(String str); + List getValuesForKeysContaining(String str, boolean caseInsensitive); int size(); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java index 4252fe155..32985a055 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java @@ -7,7 +7,7 @@ import java.util.List; public class SimpleIndex implements SearchIndex { private final List keys = new ArrayList<>(); - private final List values = new ArrayList<>(); + private final List values = new ArrayList(); @Override public void put(String str, T value) { @@ -26,14 +26,20 @@ public class SimpleIndex implements SearchIndex { } @Override - public List getValuesForKeysContaining(String str) { + public List getValuesForKeysContaining(String str, boolean caseInsensitive) { int size = size(); if (size == 0) { return Collections.emptyList(); } + if (caseInsensitive) { + str = str.toLowerCase(); + } List results = new ArrayList<>(); for (int i = 0; i < size; i++) { String key = keys.get(i); + if (caseInsensitive) { + key = key.toLowerCase(); + } if (key.contains(str)) { results.add(values.get(i)); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java b/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java index bf6f78048..92eb375af 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java @@ -1,5 +1,6 @@ package jadx.gui.utils.search; +import static jadx.gui.utils.Utils.caseChar; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -68,13 +69,21 @@ public class StringRef implements CharSequence { return indexOf(str, 0); } + public int indexOf(String str, boolean caseInsensitive) { + return indexOf(str, 0, caseInsensitive); + } + + public int indexOf(String str, int from, boolean caseInsensitive) { + return indexOf(refStr, offset, length, str, 0, str.length(), from, caseInsensitive); + } + public int indexOf(String str, int from) { - return indexOf(refStr, offset, length, str, 0, str.length(), from); + return indexOf(refStr, offset, length, str, 0, str.length(), from, false); } private static int indexOf(String source, int sourceOffset, int sourceCount, String target, int targetOffset, int targetCount, - int fromIndex) { + int fromIndex, boolean caseInsensitive) { if (fromIndex >= sourceCount) { return (targetCount == 0 ? sourceCount : -1); } @@ -84,18 +93,18 @@ public class StringRef implements CharSequence { if (targetCount == 0) { return -1; } - char first = target.charAt(targetOffset); + char first = caseChar(target.charAt(targetOffset), caseInsensitive); int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) { - if (source.charAt(i) != first) { - while (++i <= max && source.charAt(i) != first) { + if (caseChar(source.charAt(i), caseInsensitive) != first) { + while (++i <= max && caseChar(source.charAt(i), caseInsensitive) != first) { } } if (i <= max) { int j = i + 1; int end = j + targetCount - 1; int k = targetOffset + 1; - while (j < end && source.charAt(j) == target.charAt(k)) { + while (j < end && caseChar(source.charAt(j), caseInsensitive) == caseChar(target.charAt(k), caseInsensitive)) { j++; k++; } @@ -117,7 +126,7 @@ public class StringRef implements CharSequence { List list = new ArrayList<>(); while (true) { int start = pos + targetLen; - pos = indexOf(str, 0, len, splitBy, 0, targetLen, start); + pos = indexOf(str, 0, len, splitBy, 0, targetLen, start, false); if (pos == -1) { if (start != len) { list.add(subString(str, start, len)); @@ -178,4 +187,5 @@ public class StringRef implements CharSequence { int offset = this.offset; return refStr.substring(offset, offset + len); } + } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java index ca702504c..ccc13af29 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java @@ -73,22 +73,22 @@ public class TextSearchIndex { } } - public List searchClsName(String text) { - return clsNamesIndex.getValuesForKeysContaining(text); + public List searchClsName(String text, boolean caseInsensitive) { + return clsNamesIndex.getValuesForKeysContaining(text, caseInsensitive); } - public List searchMthName(String text) { - return mthNamesIndex.getValuesForKeysContaining(text); + public List searchMthName(String text, boolean caseInsensitive) { + return mthNamesIndex.getValuesForKeysContaining(text, caseInsensitive); } - public List searchFldName(String text) { - return fldNamesIndex.getValuesForKeysContaining(text); + public List searchFldName(String text, boolean caseInsensitive) { + return fldNamesIndex.getValuesForKeysContaining(text, caseInsensitive); } - public List searchCode(String text) { + public List searchCode(String text, boolean caseInsensitive) { List items; if (codeIndex.size() > 0) { - items = codeIndex.getValuesForKeysContaining(text); + items = codeIndex.getValuesForKeysContaining(text, caseInsensitive); if (skippedClasses.isEmpty()) { return items; } @@ -106,7 +106,7 @@ public class TextSearchIndex { while (pos != -1) { pos = searchNext(list, text, javaClass, code, pos); } - if (list.size() > CommonSearchDialog.MAX_RESULTS_COUNT) { + if (list.size() > CommonSearchDialog.RESULTS_PER_PAGE) { return; } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 00b887a4f..577311e17 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -49,6 +49,11 @@ search_dialog.class=Class search_dialog.method=Method search_dialog.field=Field search_dialog.code=Code +search_dialog.options=Search options \: +search_dialog.ignorecase=Case insensitive +search_dialog.next_page=Show next page +search_dialog.prev_page=Show previous page +search_dialog.info_label=Showing results %d to %d of %d usage_dialog.title=Usage search usage_dialog.label=Usage for: