diff --git a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java index 71d80e222..e9be13a9e 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java @@ -1,6 +1,7 @@ package jadx.gui; import jadx.cli.JadxCLIArgs; +import jadx.gui.ui.MainWindow; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -19,17 +20,17 @@ public class JadxGUI { SwingUtilities.invokeLater(new Runnable() { public void run() { JadxWrapper wrapper = new JadxWrapper(jadxArgs); - MainWindow mainWindow = new MainWindow(wrapper); - mainWindow.pack(); - mainWindow.setLocationAndPosition(); - mainWindow.setVisible(true); - mainWindow.setLocationRelativeTo(null); - mainWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + MainWindow window = new MainWindow(wrapper); + window.pack(); + window.setLocationAndPosition(); + window.setVisible(true); + window.setLocationRelativeTo(null); + window.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); if (jadxArgs.getInput().isEmpty()) { - mainWindow.openFile(); + window.openFile(); } else { - mainWindow.openFile(jadxArgs.getInput().get(0)); + window.openFile(jadxArgs.getInput().get(0)); } } }); diff --git a/jadx-gui/src/main/java/jadx/gui/JadxTextArea.java b/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java similarity index 67% rename from jadx-gui/src/main/java/jadx/gui/JadxTextArea.java rename to jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java index e6ddc8aa0..b8b8b40ba 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxTextArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CodeArea.java @@ -1,12 +1,18 @@ -package jadx.gui; +package jadx.gui.ui; import jadx.api.CodePosition; import jadx.gui.treemodel.JClass; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.DefaultCaret; import java.awt.Color; +import java.awt.Point; +import java.awt.Rectangle; import org.fife.ui.rsyntaxtextarea.LinkGenerator; import org.fife.ui.rsyntaxtextarea.LinkGeneratorResult; @@ -18,8 +24,8 @@ import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class JadxTextArea extends RSyntaxTextArea { - private static final Logger LOG = LoggerFactory.getLogger(JadxTextArea.class); +class CodeArea extends RSyntaxTextArea { + private static final Logger LOG = LoggerFactory.getLogger(CodeArea.class); private static final long serialVersionUID = 6312736869579635796L; @@ -27,13 +33,12 @@ public class JadxTextArea extends RSyntaxTextArea { private static final Color JUMP_FOREGROUND = new Color(0x785523); private static final Color JUMP_BACKGROUND = new Color(0xE6E6FF); + private final CodePanel codePanel; private final JClass cls; - private final MainWindow rootWindow; - - public JadxTextArea(MainWindow mainWindow, JClass cls) { - this.rootWindow = mainWindow; - this.cls = cls; + CodeArea(CodePanel panel) { + this.codePanel = panel; + this.cls = panel.getCls(); setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); SyntaxScheme scheme = getSyntaxScheme(); @@ -43,7 +48,11 @@ public class JadxTextArea extends RSyntaxTextArea { setBackground(BACKGROUND); setAntiAliasingEnabled(true); setEditable(false); - getCaret().setVisible(true); + Caret caret = getCaret(); + if (caret instanceof DefaultCaret) { + ((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); + } + caret.setVisible(true); setHyperlinksEnabled(true); CodeLinkGenerator codeLinkProcessor = new CodeLinkGenerator(cls); @@ -82,6 +91,46 @@ public class JadxTextArea extends RSyntaxTextArea { } } + void scrollToLine(int line) { + line--; + if (line < 0) { + line = 0; + } + setCaretAtLine(line); + centerCurrentLine(); + forceCurrentLineHighlightRepaint(); + } + + public void centerCurrentLine() { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, this); + if (viewport == null) { + return; + } + try { + Rectangle r = modelToView(getCaretPosition()); + if (r == null) { + return; + } + int extentHeight = viewport.getExtentSize().height; + int viewHeight = viewport.getViewSize().height; + + int y = Math.max(0, r.y - (extentHeight / 2)); + y = Math.min(y, viewHeight - extentHeight); + + viewport.setViewPosition(new Point(0, y)); + } catch (BadLocationException e) { + LOG.debug("Can't center current line", e); + } + } + + private void setCaretAtLine(int line) { + try { + setCaretPosition(getLineStartOffset(line)); + } catch (BadLocationException e) { + LOG.debug("Can't scroll to " + line, e); + } + } + private class CodeLinkGenerator implements LinkGenerator, HyperlinkListener { private final JClass jCls; @@ -124,7 +173,7 @@ public class JadxTextArea extends RSyntaxTextArea { if (obj instanceof CodePosition) { CodePosition pos = (CodePosition) obj; JClass cls = new JClass(pos.getJavaClass()); - rootWindow.showCode(cls, pos.getLine()); + codePanel.getCodePanel().showCode(cls, pos.getLine()); LOG.debug("Code jump to: {}", pos); } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java new file mode 100644 index 000000000..00633231a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/CodePanel.java @@ -0,0 +1,65 @@ +package jadx.gui.ui; + +import jadx.gui.treemodel.JClass; +import jadx.gui.utils.Utils; + +import javax.swing.AbstractAction; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import org.fife.ui.rtextarea.RTextScrollPane; + +class CodePanel extends JPanel { + + private final TabbedPane codePanel; + private final JClass jClass; + private final SearchBar searchBar; + private final CodeArea codeArea; + private final RTextScrollPane scrollPane; + + CodePanel(TabbedPane panel, JClass cls) { + codePanel = panel; + jClass = cls; + codeArea = new CodeArea(this); + searchBar = new SearchBar(codeArea); + + scrollPane = new RTextScrollPane(codeArea); + scrollPane.setFoldIndicatorEnabled(true); + + setLayout(new BorderLayout()); + add(searchBar, BorderLayout.NORTH); + add(scrollPane); + + KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK); + Utils.addKeyBinding(codeArea, key, "SearchAction", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + searchBar.toggle(); + } + }); + } + + TabbedPane getCodePanel() { + return codePanel; + } + + JClass getCls() { + return jClass; + } + + SearchBar getSearchBar() { + return searchBar; + } + + CodeArea getCodeArea() { + return codeArea; + } + + RTextScrollPane getScrollPane() { + return scrollPane; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java similarity index 68% rename from jadx-gui/src/main/java/jadx/gui/MainWindow.java rename to jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 1f26ac297..921aeb65f 100644 --- a/jadx-gui/src/main/java/jadx/gui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -1,36 +1,29 @@ -package jadx.gui; +package jadx.gui.ui; +import jadx.gui.JadxWrapper; 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.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; -import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; -import javax.swing.JTabbedPane; -import javax.swing.JTextArea; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.JTree; -import javax.swing.KeyStroke; import javax.swing.ProgressMonitor; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.filechooser.FileNameExtensionFilter; -import javax.swing.plaf.basic.BasicButtonUI; -import javax.swing.text.BadLocationException; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; @@ -40,21 +33,16 @@ import javax.swing.tree.TreeSelectionModel; import java.awt.BorderLayout; import java.awt.Component; import java.awt.DisplayMode; -import java.awt.FlowLayout; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; -import java.util.HashMap; -import java.util.Map; -import org.fife.ui.rtextarea.RTextScrollPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,7 +58,6 @@ public class MainWindow extends JFrame { private static final ImageIcon ICON_OPEN = Utils.openIcon("folder"); private static final ImageIcon ICON_SAVE_ALL = Utils.openIcon("disk_multiple"); private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross"); - private static final ImageIcon ICON_CLOSE_INACTIVE = Utils.openIcon("cross_grayed"); private static final ImageIcon ICON_FLAT_PKG = Utils.openIcon("empty_logical_package_obj"); private static final ImageIcon ICON_SEARCH = Utils.openIcon("magnifier"); @@ -80,9 +67,7 @@ public class MainWindow extends JFrame { private JTree tree; private DefaultTreeModel treeModel; - - private final JTabbedPane tabbedPane = new JTabbedPane(); - private final Map openTabs = new HashMap(); + private TabbedPane tabbedPane; public MainWindow(JadxWrapper wrapper) { this.wrapper = wrapper; @@ -138,126 +123,19 @@ public class MainWindow extends JFrame { } } - private void toggleSearch() { - SearchBar searchBar = getSearchBar((JPanel) tabbedPane.getSelectedComponent()); - searchBar.toggle(); - } - private void treeClickAction() { Object obj = tree.getLastSelectedPathComponent(); if (obj instanceof JNode) { JNode node = (JNode) obj; JClass cls = node.getRootClass(); if (cls != null) { - showCode(cls, node.getLine()); + tabbedPane.showCode(cls, node.getLine()); } } } - private JPanel newCodePane(final JClass cls) { - JadxTextArea textArea = new JadxTextArea(this, cls); - RTextScrollPane scrollPane = new RTextScrollPane(textArea); - scrollPane.setFoldIndicatorEnabled(true); - - JPanel textPanel = new JPanel(new BorderLayout()); - SearchBar searchBar = new SearchBar(textArea); - textPanel.add(searchBar, BorderLayout.NORTH); - textPanel.add(scrollPane); - - KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK); - Utils.addKeyBinding(textArea, key, "SearchAction", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - toggleSearch(); - } - }); - return textPanel; - } - - private SearchBar getSearchBar(JPanel panel) { - return (SearchBar) panel.getComponent(0); - } - - private JadxTextArea getTextArea(JPanel panel) { - RTextScrollPane scrollPane = (RTextScrollPane) panel.getComponent(1); - return (JadxTextArea) scrollPane.getTextArea(); - } - - void showCode(JClass cls, int line) { - JPanel panel = (JPanel) openTabs.get(cls); - if (panel != null) { - panel = (JPanel) openTabs.get(cls); - tabbedPane.setSelectedComponent(panel); - } else { - panel = newCodePane(cls); - tabbedPane.add(panel); - openTabs.put(cls, panel); - int id = tabbedPane.indexOfComponent(panel); - tabbedPane.setTabComponentAt(id, makeTabComponent(cls, panel)); - tabbedPane.setSelectedIndex(id); - } - JadxTextArea textArea = getTextArea(panel); - scrollToLine(textArea, line); - } - - private Component makeTabComponent(final JClass cls, final Component comp) { - String name = cls.getCls().getFullName(); - JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 0)); - panel.setOpaque(false); - - final JLabel label = new JLabel(name); - label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); - label.setIcon(cls.getIcon()); - - final JButton button = new JButton(); - button.setIcon(ICON_CLOSE_INACTIVE); - button.setRolloverIcon(ICON_CLOSE); - button.setRolloverEnabled(true); - button.setOpaque(false); - button.setUI(new BasicButtonUI()); - button.setContentAreaFilled(false); - button.setFocusable(false); - button.setBorder(null); - button.setBorderPainted(false); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - closeCodeTab(cls, comp); - } - }); - - panel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON2) { - closeCodeTab(cls, comp); - } else { - // TODO: make correct event delegation to tabbed pane - tabbedPane.setSelectedComponent(comp); - } - } - }); - - panel.add(label); - panel.add(button); - panel.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); - return panel; - } - - private void closeCodeTab(JClass cls, Component comp) { - tabbedPane.remove(comp); - openTabs.remove(cls); - } - - private void scrollToLine(JTextArea textArea, int line) { - if (line == 0) { - line = 1; - } - try { - textArea.setCaretPosition(textArea.getLineStartOffset(line - 1)); - } catch (BadLocationException e) { - LOG.error("Can't scroll to " + line, e); - } + private void toggleSearch() { + tabbedPane.getSelectedCodePanel().getSearchBar().toggle(); } private void initMenuAndToolbar() { @@ -396,7 +274,7 @@ public class MainWindow extends JFrame { JScrollPane treeScrollPane = new JScrollPane(tree); splitPane.setLeftComponent(treeScrollPane); - tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT); + tabbedPane = new TabbedPane(this); splitPane.setRightComponent(tabbedPane); setContentPane(mainPanel); diff --git a/jadx-gui/src/main/java/jadx/gui/SearchBar.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchBar.java similarity index 98% rename from jadx-gui/src/main/java/jadx/gui/SearchBar.java rename to jadx-gui/src/main/java/jadx/gui/ui/SearchBar.java index 6478088b7..0a381ed66 100644 --- a/jadx-gui/src/main/java/jadx/gui/SearchBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchBar.java @@ -1,4 +1,4 @@ -package jadx.gui; +package jadx.gui.ui; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; @@ -20,7 +20,7 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; -public class SearchBar extends JToolBar { +class SearchBar extends JToolBar { private static final long serialVersionUID = 1836871286618633003L; private static final Color COLOR_BG_ERROR = new Color(0xFFDFDE); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java new file mode 100644 index 000000000..8566f9d7a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -0,0 +1,112 @@ +package jadx.gui.ui; + +import jadx.gui.treemodel.JClass; +import jadx.gui.utils.Utils; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.plaf.basic.BasicButtonUI; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.HashMap; +import java.util.Map; + +class TabbedPane extends JTabbedPane { + + private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross"); + private static final ImageIcon ICON_CLOSE_INACTIVE = Utils.openIcon("cross_grayed"); + + private final MainWindow mainWindow; + private final Map openTabs = new HashMap(); + + TabbedPane(MainWindow window) { + mainWindow = window; + + setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + } + + private void addCodePanel(CodePanel codePanel) { + add(codePanel); + openTabs.put(codePanel.getCls(), codePanel); + } + + private void closeCodePanel(CodePanel codePanel) { + remove(codePanel); + openTabs.remove(codePanel.getCls()); + } + + void showCode(JClass cls, int line) { + CodePanel panel = openTabs.get(cls); + if (panel == null) { + panel = new CodePanel(this, cls); + addCodePanel(panel); + setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel)); + } + + setSelectedComponent(panel); + CodeArea codeArea = panel.getCodeArea(); + codeArea.scrollToLine(line); + codeArea.requestFocus(); + } + + private Component makeTabComponent(final CodePanel codePanel) { + JClass cls = codePanel.getCls(); + String name = cls.getCls().getFullName(); + JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 0)); + panel.setOpaque(false); + + final JLabel label = new JLabel(name); + label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); + label.setIcon(cls.getIcon()); + + final JButton button = new JButton(); + button.setIcon(ICON_CLOSE_INACTIVE); + button.setRolloverIcon(ICON_CLOSE); + button.setRolloverEnabled(true); + button.setOpaque(false); + button.setUI(new BasicButtonUI()); + button.setContentAreaFilled(false); + button.setFocusable(false); + button.setBorder(null); + button.setBorderPainted(false); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + closeCodePanel(codePanel); + } + }); + + panel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON2) { + closeCodePanel(codePanel); + } else { + // TODO: make correct event delegation to tabbed pane + setSelectedComponent(codePanel); + } + } + }); + + panel.add(label); + panel.add(button); + panel.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); + return panel; + } + + CodePanel getSelectedCodePanel() { + return (CodePanel) getSelectedComponent(); + } + + MainWindow getMainWindow() { + return mainWindow; + } +}