diff --git a/jadx-gui/src/main/java/jadx/gui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/MainWindow.java index a171ab1c2..ed94933b0 100644 --- a/jadx-gui/src/main/java/jadx/gui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/MainWindow.java @@ -4,7 +4,10 @@ import jadx.cli.JadxArgs; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRoot; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; +import javax.swing.AbstractAction; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; @@ -18,9 +21,9 @@ import javax.swing.JSplitPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.JTree; +import javax.swing.KeyStroke; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; -import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.text.BadLocationException; import javax.swing.tree.DefaultMutableTreeNode; @@ -32,6 +35,7 @@ import java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.File; @@ -50,12 +54,15 @@ public class MainWindow extends JFrame { private static final ImageIcon ICON_OPEN = Utils.openIcon("folder"); private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross"); private static final ImageIcon ICON_FLAT_PKG = Utils.openIcon("empty_logical_package_obj"); + private static final ImageIcon ICON_SEARCH = Utils.openIcon("magnifier"); private final JadxWrapper wrapper; private JPanel mainPanel; private JTree tree; private DefaultTreeModel treeModel; private RSyntaxTextArea textArea; + private JToolBar searchToolBar; + private SearchBar searchBar; public MainWindow(JadxArgs jadxArgs) { this.wrapper = new JadxWrapper(jadxArgs); @@ -87,6 +94,10 @@ public class MainWindow extends JFrame { } } + private void toggleSearch() { + searchBar.toggle(); + } + private void treeClickAction() { Object obj = tree.getLastSelectedPathComponent(); if (obj instanceof JNode) { @@ -94,8 +105,8 @@ public class MainWindow extends JFrame { if (node.getJParent() != null) { textArea.setText(node.getJParent().getCode()); scrollToLine(node.getLine()); - } else if (node.getClass() == JClass.class){ - textArea.setText(((JClass)node).getCode()); + } else if (node.getClass() == JClass.class) { + textArea.setText(((JClass) node).getCode()); scrollToLine(node.getLine()); } } @@ -149,7 +160,7 @@ public class MainWindow extends JFrame { toolbar.add(openButton); toolbar.addSeparator(); - JToggleButton flatPkgButton = new JToggleButton(ICON_FLAT_PKG); + final JToggleButton flatPkgButton = new JToggleButton(ICON_FLAT_PKG); flatPkgButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -158,15 +169,35 @@ public class MainWindow extends JFrame { }); flatPkgButton.setToolTipText(NLS.str("tree.flatten")); toolbar.add(flatPkgButton); + toolbar.addSeparator(); + + final JButton searchButton = new JButton(ICON_SEARCH); + searchButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + toggleSearch(); + } + }); + searchButton.setToolTipText(NLS.str("search")); + toolbar.add(searchButton); + + KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK); + Utils.addKeyBinding(textArea, key, "SearchAction", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + toggleSearch(); + } + }); toolbar.addSeparator(); - add(toolbar, BorderLayout.NORTH); + mainPanel.add(toolbar, BorderLayout.NORTH); } private void initUI() { mainPanel = new JPanel(new BorderLayout()); JSplitPane splitPane = new JSplitPane(); + splitPane.setDividerLocation(200); mainPanel.add(splitPane); DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("Please open file"); @@ -198,7 +229,7 @@ public class MainWindow extends JFrame { JScrollPane treeScrollPane = new JScrollPane(tree); splitPane.setLeftComponent(treeScrollPane); - textArea = new RSyntaxTextArea(20, 60); + textArea = new RSyntaxTextArea(); textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); textArea.setMarkOccurrences(true); textArea.setBackground(BACKGROUND); @@ -206,9 +237,15 @@ public class MainWindow extends JFrame { textArea.setAntiAliasingEnabled(true); // textArea.setHyperlinksEnabled(true); textArea.setTabSize(4); + RTextScrollPane scrollPane = new RTextScrollPane(textArea); scrollPane.setFoldIndicatorEnabled(true); - splitPane.setRightComponent(scrollPane); + + JPanel textPanel = new JPanel(new BorderLayout()); + searchBar = new SearchBar(textArea); + textPanel.add(searchBar.getToolBar(), BorderLayout.NORTH); + textPanel.add(scrollPane); + splitPane.setRightComponent(textPanel); setContentPane(mainPanel); setTitle(DEFAULT_TITLE); @@ -220,12 +257,10 @@ public class MainWindow extends JFrame { private class OpenListener implements ActionListener { public void actionPerformed(ActionEvent event) { JFileChooser fileChooser = new JFileChooser(); - FileFilter filter = new FileNameExtensionFilter("dex files", "dex", "apk", "jar"); - fileChooser.addChoosableFileFilter(filter); + fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("dex files", "dex", "apk", "jar")); int ret = fileChooser.showDialog(mainPanel, "Open file"); if (ret == JFileChooser.APPROVE_OPTION) { - File file = fileChooser.getSelectedFile(); - openFile(file); + openFile(fileChooser.getSelectedFile()); } } } diff --git a/jadx-gui/src/main/java/jadx/gui/SearchBar.java b/jadx-gui/src/main/java/jadx/gui/SearchBar.java new file mode 100644 index 000000000..9a0c14d49 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/SearchBar.java @@ -0,0 +1,205 @@ +package jadx.gui; + +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JToolBar; +import javax.swing.text.BadLocationException; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rtextarea.SearchContext; +import org.fife.ui.rtextarea.SearchEngine; + +public class SearchBar { + private static final Color COLOR_BG_ERROR = new Color(0xFFDFDE); + private static final Color COLOR_BG_WARN = new Color(0xFFFDD9); + private static final Color COLOR_BG_NORMAL = new Color(0xFFFFFF); + + private static final Icon ICON_UP = Utils.openIcon("arrow_up"); + private static final Icon ICON_DOWN = Utils.openIcon("arrow_down"); + private static final Icon ICON_CLOSE = Utils.openIcon("cross"); + + private final RSyntaxTextArea rTextArea; + private final JToolBar toolBar; + + private final JTextField searchField; + private final JCheckBox markAllCB; + private final JCheckBox regexCB; + + private final JCheckBox wholeWordCB; + private final JCheckBox matchCaseCB; + + public SearchBar(RSyntaxTextArea textArea) { + rTextArea = textArea; + toolBar = new JToolBar(); + + JLabel findLabel = new JLabel(NLS.str("search.find") + ":"); + toolBar.add(findLabel); + + searchField = new JTextField(30); + searchField.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_ENTER: + // skip + break; + case KeyEvent.VK_ESCAPE: + toggle(); + break; + default: + search(0); + break; + } + } + }); + searchField.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + search(1); + } + }); + toolBar.add(searchField); + + JButton prevButton = new JButton(NLS.str("search.previous")); + prevButton.setIcon(ICON_UP); + prevButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + search(-1); + } + }); + toolBar.add(prevButton); + + JButton nextButton = new JButton(NLS.str("search.next")); + nextButton.setIcon(ICON_DOWN); + nextButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + search(1); + } + }); + toolBar.add(nextButton); + + markAllCB = new JCheckBox(NLS.str("search.mark_all")); + markAllCB.addActionListener(new ForwardListener()); + toolBar.add(markAllCB); + + regexCB = new JCheckBox(NLS.str("search.regex")); + regexCB.addActionListener(new ForwardListener()); + toolBar.add(regexCB); + + matchCaseCB = new JCheckBox(NLS.str("search.match_case")); + matchCaseCB.addActionListener(new ForwardListener()); + toolBar.add(matchCaseCB); + + wholeWordCB = new JCheckBox(NLS.str("search.whole_word")); + wholeWordCB.addActionListener(new ForwardListener()); + toolBar.add(wholeWordCB); + + JButton closeButton = new JButton(); + closeButton.setIcon(ICON_CLOSE); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + toggle(); + } + }); + toolBar.add(closeButton); + + toolBar.setFloatable(false); + toolBar.setVisible(false); + } + + public JToolBar getToolBar() { + return toolBar; + } + + public boolean toggle() { + boolean visible = !toolBar.isVisible(); + toolBar.setVisible(visible); + + if (visible) { + searchField.requestFocus(); + } else { + rTextArea.requestFocus(); + } + return visible; + } + + private void search(int direction) { + String text = searchField.getText(); + if (text.length() == 0) { + return; + } + + boolean forward = (direction >= 0); + boolean matchCase = matchCaseCB.isSelected(); + boolean regex = regexCB.isSelected(); + boolean wholeWord = wholeWordCB.isSelected(); + + if (markAllCB.isSelected()) { + rTextArea.markAll(text, matchCase, wholeWord, regex); + } else { + rTextArea.clearMarkAllHighlights(); + } + + SearchContext context = new SearchContext(); + context.setSearchFor(text); + context.setMatchCase(matchCase); + context.setRegularExpression(regex); + context.setSearchForward(forward); + context.setWholeWord(wholeWord); + + // TODO hack: move cursor before previous search for not jump to next occurrence + if (direction == 0 && !searchField.getBackground().equals(COLOR_BG_ERROR)) { + try { + int caretPos = rTextArea.getCaretPosition(); + int lineNum = rTextArea.getLineOfOffset(caretPos) - 1; + if (lineNum > 1) { + rTextArea.setCaretPosition(rTextArea.getLineStartOffset(lineNum)); + } + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + boolean found = SearchEngine.find(rTextArea, context); + if (!found) { + int pos = SearchEngine.getNextMatchPos(text, rTextArea.getText(), forward, matchCase, wholeWord); + if (pos != -1) { + rTextArea.setCaretPosition(forward ? 0 : rTextArea.getDocument().getLength() - 1); + search(direction); + searchField.setBackground(COLOR_BG_WARN); + return; + } + searchField.setBackground(COLOR_BG_ERROR); + } else { + searchField.setBackground(COLOR_BG_NORMAL); + } + } + + private class ForwardListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + search(0); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 822cee4d9..d83910ab2 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -4,7 +4,7 @@ import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.core.dex.info.AccessInfo; -import jadx.gui.Utils; +import jadx.gui.utils.Utils; import javax.swing.Icon; import javax.swing.ImageIcon; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java index aa3eb22c5..e840dd084 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java @@ -2,7 +2,7 @@ package jadx.gui.treemodel; import jadx.api.JavaField; import jadx.core.dex.info.AccessInfo; -import jadx.gui.Utils; +import jadx.gui.utils.Utils; import javax.swing.Icon; import javax.swing.ImageIcon; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java index 7c4c83e3c..e1c9378c0 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java @@ -2,7 +2,7 @@ package jadx.gui.treemodel; import jadx.api.JavaMethod; import jadx.core.dex.info.AccessInfo; -import jadx.gui.Utils; +import jadx.gui.utils.Utils; import javax.swing.Icon; import javax.swing.ImageIcon; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java index 7480c5575..af2c98767 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java @@ -2,7 +2,7 @@ package jadx.gui.treemodel; import jadx.api.JavaClass; import jadx.api.JavaPackage; -import jadx.gui.Utils; +import jadx.gui.utils.Utils; import javax.swing.Icon; import javax.swing.ImageIcon; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java index 7e0912c70..41c24dcf3 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java @@ -2,7 +2,7 @@ package jadx.gui.treemodel; import jadx.api.JavaPackage; import jadx.gui.JadxWrapper; -import jadx.gui.Utils; +import jadx.gui.utils.Utils; import javax.swing.Icon; import javax.swing.ImageIcon; diff --git a/jadx-gui/src/main/java/jadx/gui/NLS.java b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java similarity index 93% rename from jadx-gui/src/main/java/jadx/gui/NLS.java rename to jadx-gui/src/main/java/jadx/gui/utils/NLS.java index c3bf3e2dc..ebf4aba3a 100644 --- a/jadx-gui/src/main/java/jadx/gui/NLS.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java @@ -1,4 +1,4 @@ -package jadx.gui; +package jadx.gui.utils; import java.util.Locale; import java.util.ResourceBundle; diff --git a/jadx-gui/src/main/java/jadx/gui/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java similarity index 58% rename from jadx-gui/src/main/java/jadx/gui/Utils.java rename to jadx-gui/src/main/java/jadx/gui/utils/Utils.java index f23c80101..4e34de5a4 100644 --- a/jadx-gui/src/main/java/jadx/gui/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -1,8 +1,11 @@ -package jadx.gui; +package jadx.gui.utils; import jadx.core.utils.exceptions.JadxRuntimeException; +import javax.swing.AbstractAction; import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; import java.net.URL; public class Utils { @@ -15,4 +18,9 @@ public class Utils { } return new ImageIcon(resource); } + + public static void addKeyBinding(JComponent comp, KeyStroke key, String id, AbstractAction action) { + comp.getInputMap().put(key, id); + comp.getActionMap().put(id, action); + } } 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 2f6124650..b9d0370df 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -1,2 +1,12 @@ file.open=Open file + tree.flatten=Flatten packages + +search=Search +search.previous=Previous +search.next=Next +search.mark_all=Mark All +search.regex=Regex +search.match_case=Match Case +search.whole_word=Whole word +search.find=Find diff --git a/jadx-gui/src/main/resources/icons-16/arrow_down.png b/jadx-gui/src/main/resources/icons-16/arrow_down.png new file mode 100644 index 000000000..2c4e27937 Binary files /dev/null and b/jadx-gui/src/main/resources/icons-16/arrow_down.png differ diff --git a/jadx-gui/src/main/resources/icons-16/arrow_up.png b/jadx-gui/src/main/resources/icons-16/arrow_up.png new file mode 100644 index 000000000..1ebb19324 Binary files /dev/null and b/jadx-gui/src/main/resources/icons-16/arrow_up.png differ diff --git a/jadx-gui/src/main/resources/icons-16/magnifier.png b/jadx-gui/src/main/resources/icons-16/magnifier.png new file mode 100644 index 000000000..cf3d97f75 Binary files /dev/null and b/jadx-gui/src/main/resources/icons-16/magnifier.png differ