diff --git a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java index 37127353261b378699ec31d30cb1a16b4e49ac30..9652ff8cc305de17886e67dd0ba9aef7e113f334 100644 --- a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java @@ -7,6 +7,8 @@ import jadx.core.deobf.NameMapper; public class StringUtils { private static final StringUtils DEFAULT_INSTANCE = new StringUtils(new JadxArgs()); + private static final String WHITES = " \t\r\n\f\b"; + private static final String WORD_SEPARATORS = WHITES + "(\")<,>{}=+-*/|[]\\:;'.`~!#^&"; public static StringUtils getInstance() { return DEFAULT_INSTANCE; @@ -253,4 +255,13 @@ public class StringUtils { } return count; } + + public static boolean isWhite(char chr) { + return WHITES.indexOf(chr) != -1; + } + + public static boolean isWordSeparator(char chr) { + return WORD_SEPARATORS.indexOf(chr) != -1; + + } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 2c044fa61477fb2c591dee6f53f7edc225db0e8d..7534679bf5a34ffc30fbd00e988663905d8961b3 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -62,6 +62,7 @@ public class JadxSettings extends JadxCLIArgs { private Map windowPos = new HashMap<>(); private int mainWindowExtendedState = JFrame.NORMAL; + private boolean codeAreaLineWrap = false; /** * UI setting: the width of the tree showing the classes, resources, ... */ @@ -391,6 +392,14 @@ public class JadxSettings extends JadxCLIArgs { partialSync(settings -> settings.mainWindowExtendedState = mainWindowExtendedState); } + public void setCodeAreaLineWrap(boolean lineWrap) { + this.codeAreaLineWrap = lineWrap; + } + + public boolean isCodeAreaLineWrap() { + return this.codeAreaLineWrap; + } + private void upgradeSettings(int fromVersion) { LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion == 0) { 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 7011e42375f2a5761bf2975ba6f09e1ab50fd86d..7aad721417b68a8788c2e24d28805455d8b5a54c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.swing.*; @@ -19,6 +20,7 @@ import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; +import org.fife.ui.rsyntaxtextarea.DocumentRange; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.fife.ui.rtextarea.SearchContext; @@ -36,6 +38,7 @@ import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.utils.CacheObject; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; import jadx.gui.utils.search.TextSearchIndex; public abstract class CommonSearchDialog extends JDialog { @@ -108,8 +111,9 @@ public abstract class CommonSearchDialog extends JDialog { if (selectedId == -1) { return; } + int pos = Math.max(0, resultsModel.renderer.getFirstMarkOfCode(selectedId)); JNode node = (JNode) resultsModel.getValueAt(selectedId, 0); - tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine())); + tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine(), pos)); dispose(); } @@ -121,8 +125,7 @@ public abstract class CommonSearchDialog extends JDialog { } protected void initCommon() { - KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - getRootPane().registerKeyboardAction(e -> dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); + UiUtils.addEscapeShortCutToDispose(this); } @NotNull @@ -410,10 +413,23 @@ public abstract class CommonSearchDialog extends JDialog { this.codeBackground = area.getBackground(); } + public int getFirstMarkOfCode(int row) { + Component comp = componentCache.get(makeID(row, 1)); + if (comp instanceof RSyntaxTextArea) { + List ranges = ((RSyntaxTextArea) comp).getMarkAllHighlightRanges(); + if (ranges.size() > 0) { + // minus 2 cuz the start of textArea of the column is added 2 + // spaces in makeCell method. + return ranges.get(0).getStartOffset() - 2; + } + } + return 0; + } + @Override public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, boolean hasFocus, int row, int column) { - int id = row << 2 | column; + int id = makeID(row, column); Component comp = componentCache.get(id); if (comp == null) { if (obj instanceof JNode) { @@ -427,6 +443,10 @@ public abstract class CommonSearchDialog extends JDialog { return comp; } + private int makeID(int row, int col) { + return row << 2 | col; + } + private void updateSelection(JTable table, Component comp, boolean isSelected) { if (comp instanceof RSyntaxTextArea) { if (isSelected) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java index c9d21cadf19987aae2ab235df24448af22c83f61..3eb159b4dcc151487decf6f4b9556663dd83f746 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java @@ -30,6 +30,10 @@ public final class HtmlPanel extends ContentPanel { textArea.setFont(settings.getFont()); } + public JEditorPane getHtmlArea() { + return textArea; + } + private static final class JHtmlPane extends JEditorPane { private static final long serialVersionUID = 6886040384052136157L; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java index d0d8e8c7b74060a1525f6274941902fd879ad4bd..d961df64a75580f064a3e274c71291c838bc698a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -49,10 +49,7 @@ import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.codearea.CodePanel; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.JNodeCache; -import jadx.gui.utils.NLS; -import jadx.gui.utils.TextStandardActions; +import jadx.gui.utils.*; public class RenameDialog extends JDialog { private static final long serialVersionUID = -3269715644416902410L; @@ -319,6 +316,7 @@ public class RenameDialog extends JDialog { setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.APPLICATION_MODAL); + UiUtils.addEscapeShortCutToDispose(this); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 7fdd18d2bcdd534a9090ee6afea2eed1531e5eeb..c4f4bf0564ab1dd072f00a6440f30abe315d8a18 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -1,13 +1,11 @@ package jadx.gui.ui; -import java.awt.Component; -import java.util.ArrayList; -import java.util.LinkedHashMap; +import java.awt.*; +import java.awt.event.*; +import java.util.*; import java.util.List; -import java.util.Map; -import javax.swing.JTabbedPane; -import javax.swing.SwingUtilities; +import javax.swing.*; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.Nullable; @@ -16,13 +14,12 @@ import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourceType; +import jadx.core.utils.StringUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.ApkSignature; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; -import jadx.gui.ui.codearea.AbstractCodeArea; -import jadx.gui.ui.codearea.AbstractCodeContentPanel; -import jadx.gui.ui.codearea.ClassCodeContentPanel; -import jadx.gui.ui.codearea.CodeContentPanel; +import jadx.gui.ui.codearea.*; import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; @@ -35,6 +32,9 @@ public class TabbedPane extends JTabbedPane { private final transient Map openTabs = new LinkedHashMap<>(); private final transient JumpManager jumps = new JumpManager(); + private transient ContentPanel curTab; + private transient ContentPanel lastTab; + TabbedPane(MainWindow window) { this.mainWindow = window; @@ -55,6 +55,93 @@ public class TabbedPane extends JTabbedPane { } setSelectedIndex(index); }); + interceptTabKey(); + enableSwitchingTabs(); + } + + private void interceptTabKey() { + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { + private static final int ctrlDown = KeyEvent.CTRL_DOWN_MASK; + private long ctrlInterval = 0; + + @Override + public boolean dispatchKeyEvent(KeyEvent e) { + long cur = System.currentTimeMillis(); + if (!FocusManager.isActive()) { + return false; // don't do nothing when tab is not on focus. + } + int code = e.getKeyCode(); + boolean consume = code == KeyEvent.VK_TAB; // consume Tab key event anyway + boolean isReleased = e.getID() == KeyEvent.KEY_RELEASED; + if (isReleased) { + if (code == KeyEvent.VK_CONTROL) { + ctrlInterval = cur; + } else if (code == KeyEvent.VK_TAB) { + boolean doSwitch = false; + if ((e.getModifiersEx() & ctrlDown) != 0) { + doSwitch = lastTab != null && getTabCount() > 1; + } else { + // the gap of the release of ctrl and tab is very close, nearly the same time, + // but ctrl released first. + ctrlInterval = cur - ctrlInterval; + if (ctrlInterval <= 90) { + doSwitch = lastTab != null && getTabCount() > 1; + } + } + if (doSwitch) { + setSelectedComponent(lastTab); + } + } + } else if (consume && (e.getModifiersEx() & ctrlDown) == 0) { + // switch between source and smali + if (curTab instanceof ClassCodeContentPanel) { + ((ClassCodeContentPanel) curTab).switchPanel(); + } + } + return consume; + } + }); + } + + private void enableSwitchingTabs() { + addChangeListener(e -> { + ContentPanel tab = getSelectedCodePanel(); + if (tab == null) { // all closed + curTab = null; + lastTab = null; + return; + } + FocusManager.focusOnCodePanel(tab); + if (tab == curTab) { // a tab was closed by not the current one. + if (lastTab != null && indexOfComponent(lastTab) == -1) { // lastTab was closed + setLastTabAdjacentToCurTab(); + } + return; + } + if (tab == lastTab) { + if (indexOfComponent(curTab) == -1) { // curTab was closed and lastTab is the current one. + curTab = lastTab; + setLastTabAdjacentToCurTab(); + return; + } + // it's switching between lastTab and curTab. + } + lastTab = curTab; + curTab = tab; + }); + } + + private void setLastTabAdjacentToCurTab() { + if (getTabCount() < 2) { + lastTab = null; + return; + } + int idx = indexOfComponent(curTab); + if (idx == 0) { + lastTab = (ContentPanel) getComponentAt(idx + 1); + } else { + lastTab = (ContentPanel) getComponentAt(idx - 1); + } } public MainWindow getMainWindow() { @@ -69,16 +156,36 @@ public class TabbedPane extends JTabbedPane { SwingUtilities.invokeLater(() -> { setSelectedComponent(contentPanel); AbstractCodeArea codeArea = contentPanel.getCodeArea(); - int line = pos.getLine(); - if (line < 0) { - try { - line = 1 + codeArea.getLineOfOffset(-line); - } catch (BadLocationException e) { - LOG.error("Can't get line for: {}", pos, e); - line = pos.getNode().getLine(); + if (pos.isPrecise()) { + codeArea.scrollToPos(pos.getPos()); + } else { + int line = pos.getLine(); + if (line < 0) { + try { + line = 1 + codeArea.getLineOfOffset(-line); + } catch (BadLocationException e) { + LOG.error("Can't get line for: {}", pos, e); + line = pos.getNode().getLine(); + } + } + if (pos.getPos() <= 0) { + codeArea.scrollToLine(line); + } else { + int lineNum = Math.max(0, line - 1); + try { + int offs = codeArea.getLineStartOffset(lineNum); + while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) { + offs += 1; + } + offs += pos.getPos(); + pos.setPrecise(offs); + codeArea.scrollToPos(offs); + } catch (BadLocationException e) { + e.printStackTrace(); + codeArea.scrollToLine(line); + } } } - codeArea.scrollToLine(line); codeArea.requestFocus(); }); } @@ -149,6 +256,7 @@ public class TabbedPane extends JTabbedPane { if (panel == null) { return null; } + FocusManager.listen(panel); addContentPanel(panel); setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel)); } @@ -219,5 +327,73 @@ public class TabbedPane extends JTabbedPane { closeAllTabs(); openTabs.clear(); jumps.reset(); + curTab = null; + lastTab = null; + } + + private static class FocusManager implements FocusListener { + static boolean active = false; + static FocusManager listener = new FocusManager(); + + static boolean isActive() { + return active; + } + + @Override + public void focusGained(FocusEvent e) { + active = true; + } + + @Override + public void focusLost(FocusEvent e) { + active = false; + } + + static void listen(ContentPanel pane) { + if (pane instanceof ClassCodeContentPanel) { + ((ClassCodeContentPanel) pane).getCodeArea().addFocusListener(listener); + ((ClassCodeContentPanel) pane).getSmaliCodeArea().addFocusListener(listener); + return; + } + if (pane instanceof AbstractCodeContentPanel) { + ((AbstractCodeContentPanel) pane).getCodeArea().addFocusListener(listener); + return; + } + if (pane instanceof HtmlPanel) { + ((HtmlPanel) pane).getHtmlArea().addFocusListener(listener); + return; + } + if (pane instanceof ImagePanel) { + pane.addFocusListener(listener); + return; + } + throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane); + } + + static void focusOnCodePanel(ContentPanel pane) { + if (pane instanceof ClassCodeContentPanel) { + SwingUtilities.invokeLater(() -> { + ((ClassCodeContentPanel) pane).getCurrentCodeArea().requestFocus(); + }); + return; + } + if (pane instanceof AbstractCodeContentPanel) { + SwingUtilities.invokeLater(() -> { + ((AbstractCodeContentPanel) pane).getCodeArea().requestFocus(); + }); + return; + } + if (pane instanceof HtmlPanel) { + SwingUtilities.invokeLater(() -> { + ((HtmlPanel) pane).getHtmlArea().requestFocusInWindow(); + }); + return; + } + if (pane instanceof ImagePanel) { + SwingUtilities.invokeLater(((ImagePanel) pane)::requestFocusInWindow); + return; + } + throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane); + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index ad398b08f4eb055e6409834cf3001aa8a260d7ed..796cccb235619c5d546e1e0fb892e3a97f83f780 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -1,13 +1,14 @@ package jadx.gui.ui.codearea; import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; +import java.awt.event.*; import javax.swing.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Caret; -import javax.swing.text.DefaultCaret; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import javax.swing.text.*; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; @@ -16,17 +17,26 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.core.utils.StringUtils; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.ContentPanel; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JumpPosition; +import jadx.gui.utils.NLS; public abstract class AbstractCodeArea extends RSyntaxTextArea { private static final long serialVersionUID = -3980354865216031972L; private static final Logger LOG = LoggerFactory.getLogger(AbstractCodeArea.class); + public static final Color MARK_ALL_HIGHLIGHT_COLOR = Color.decode("#FFED89"); + + @Override + public boolean getHighlightCurrentLine() { + return super.getHighlightCurrentLine(); + } + protected final ContentPanel contentPanel; protected final JNode node; @@ -38,14 +48,148 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { setEditable(false); setCodeFoldingEnabled(false); loadSettings(); + JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings(); + setLineWrap(settings.isCodeAreaLineWrap()); + setMarkAllHighlightColor(MARK_ALL_HIGHLIGHT_COLOR); + + JPopupMenu popupMenu = getPopupMenu(); + popupMenu.addSeparator(); + JCheckBoxMenuItem wrapItem = new JCheckBoxMenuItem(NLS.str("popup.line_wrap"), getLineWrap()); + wrapItem.setAction(new AbstractAction(NLS.str("popup.line_wrap")) { + @Override + public void actionPerformed(ActionEvent e) { + boolean wrap = !getLineWrap(); + settings.setCodeAreaLineWrap(wrap); + contentPanel.getTabbedPane().getOpenTabs().values().forEach(v -> { + if (v instanceof AbstractCodeContentPanel) { + ((AbstractCodeContentPanel) v).getCodeArea().setLineWrap(wrap); + } + }); + settings.sync(); + } + }); + popupMenu.add(wrapItem); + popupMenu.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + wrapItem.setState(getLineWrap()); + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + + } + }); Caret caret = getCaret(); if (caret instanceof DefaultCaret) { ((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); } - caret.setVisible(true); + this.addFocusListener(new FocusListener() { + // fix caret missing bug. + // when lost focus set visible to false, + // and when regained set back to true will force + // the caret to be repainted. + @Override + public void focusGained(FocusEvent e) { + caret.setVisible(true); + } - registerWordHighlighter(); + @Override + public void focusLost(FocusEvent e) { + caret.setVisible(false); + } + }); + addCaretListener(new CaretListener() { + int lastPos = -1; + String lastText = ""; + + @Override + public void caretUpdate(CaretEvent e) { + int pos = e.getDot(); + if (pos == 0) { + // not accepting 0, cuz sometimes the underlying RSyntaxTextArea + // will fire a fake event to force repaint caret, and its dot is + // usually 0, so we just ignore 0 anyway as a workaround. + return; + } + if (lastPos != pos) { + lastPos = pos; + lastText = highlightCaretWord(lastText, pos); + } + } + }); + } + + private String highlightCaretWord(String lastText, int pos) { + String text = getWordByPosition(pos); + if (StringUtils.isEmpty(text)) { + highlightAllMatches(null); + lastText = ""; + } else if (!lastText.equals(text)) { + highlightAllMatches(text); + lastText = text; + } + return lastText; + } + + public String getWordUnderCaret() { + return getWordByPosition(getCaretPosition()); + } + + public int getWordStart(int pos) { + int start = Math.max(0, pos - 1); + try { + if (!StringUtils.isWordSeparator(getText(start, 1).charAt(0))) { + do { + start--; + } while (start >= 0 && !StringUtils.isWordSeparator(getText(start, 1).charAt(0))); + } + start++; + } catch (BadLocationException e) { + e.printStackTrace(); + start = -1; + } + return start; + } + + public int getWordEnd(int pos, int max) { + int end = pos; + try { + if (!StringUtils.isWordSeparator(getText(end, 1).charAt(0))) { + do { + end++; + } while (end < max && !StringUtils.isWordSeparator(getText(end, 1).charAt(0))); + } + } catch (BadLocationException e) { + e.printStackTrace(); + end = max; + } + return end; + } + + public String getWordByPosition(int pos) { + String text; + int len = getDocument().getLength(); + int start = getWordStart(pos); + int end = getWordEnd(pos, len); + try { + if (end > start) { + text = getText(start, end - start); + } else { + text = null; + } + } catch (BadLocationException e) { + e.printStackTrace(); + System.out.printf("start: %d end: %d%n", start, end); + text = null; + } + return text; } /** @@ -79,6 +223,16 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { loadCommonSettings(contentPanel.getTabbedPane().getMainWindow(), this); } + public void scrollToPos(int pos) { + try { + setCaretPosition(pos); + } catch (Exception e) { + LOG.debug("Can't scroll to position {}", pos, e); + } + centerCurrentLine(); + forceCurrentLineHighlightRepaint(); + } + public void scrollToLine(int line) { int lineNum = line - 1; if (lineNum < 0) { @@ -153,7 +307,9 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { } public JumpPosition getCurrentPosition() { - return new JumpPosition(node, getCaretLineNumber() + 1); + JumpPosition jp = new JumpPosition(node, getCaretLineNumber() + 1); + jp.setPrecise(getCaretPosition()); + return jp; } @Nullable diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index 937795ef5c62d14cf95da117e75fe63965a84958..cbfc6c9b934b2173218af13a1721841503e410ae 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -22,7 +22,7 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel { private final transient CodePanel javaCodePanel; private final transient CodePanel smaliCodePanel; - // private final transient JTabbedPane areaTabbedPane; + private final transient JTabbedPane areaTabbedPane; public ClassCodeContentPanel(TabbedPane panel, JNode jnode) { super(panel, jnode); @@ -33,7 +33,7 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel { setLayout(new BorderLayout()); setBorder(new EmptyBorder(0, 0, 0, 0)); - JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); + areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0)); areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code")); @@ -74,4 +74,17 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel { return javaCodePanel; } + public void switchPanel() { + boolean toSmali = areaTabbedPane.getSelectedComponent() == javaCodePanel; + areaTabbedPane.setSelectedComponent(toSmali ? smaliCodePanel : javaCodePanel); + } + + public AbstractCodeArea getCurrentCodeArea() { + return ((CodePanel) areaTabbedPane.getSelectedComponent()).getCodeArea(); + } + + public AbstractCodeArea getSmaliCodeArea() { + return smaliCodePanel.getCodeArea(); + } + } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index a69202ac6eb9b5b39515525650122836448414c4..7c54ebc1e38d64f421c890e15cfa7cd27e268a79 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -1,8 +1,7 @@ package jadx.gui.ui.codearea; -import java.awt.event.InputEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; +import java.awt.*; +import java.awt.event.*; import javax.swing.*; @@ -50,20 +49,25 @@ public final class CodeArea extends AbstractCodeArea { @SuppressWarnings("deprecation") @Override public void mouseClicked(MouseEvent e) { - if (e.isControlDown()) { - int offs = viewToModel(e.getPoint()); - JumpPosition jump = codeLinkGenerator.getJumpLinkAtOffset(CodeArea.this, offs); - if (jump != null) { - contentPanel.getTabbedPane().codeJump(jump); - } + if (e.getClickCount() % 2 == 0 || e.isControlDown()) { + navToDecl(e.getPoint(), codeLinkGenerator); } } }); + if (isJavaCode) { addMouseMotionListener(new MouseHoverHighlighter(this, codeLinkGenerator)); } } + private void navToDecl(Point point, CodeLinkGenerator codeLinkGenerator) { + int offs = viewToModel(point); + JumpPosition jump = codeLinkGenerator.getJumpLinkAtOffset(CodeArea.this, offs); + if (jump != null) { + contentPanel.getTabbedPane().codeJump(jump); + } + } + @Override public void load() { if (getText().isEmpty()) { @@ -149,6 +153,14 @@ public final class CodeArea extends AbstractCodeArea { return nodeCache.makeFrom(javaNode); } + public JNode getNodeUnderCaret() { + int start = getWordStart(getCaretPosition()); + if (start == -1) { + start = getCaretPosition(); + } + return getJNodeAtOffset(start); + } + @Nullable public JNode getJNodeAtOffset(int offset) { JavaNode javaNode = getJavaNodeAtOffset(offset); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java index 47fbc01f8b41586744c9054f34b82b4f12185192..ed7575b6c115965ce249283e1aece8de92cc783d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java @@ -1,6 +1,9 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.*; import org.jetbrains.annotations.Nullable; @@ -8,20 +11,35 @@ import jadx.gui.treemodel.JNode; import jadx.gui.ui.UsageDialog; import jadx.gui.utils.NLS; +import static javax.swing.KeyStroke.getKeyStroke; + public final class FindUsageAction extends JNodeMenuAction { private static final long serialVersionUID = 4692546569977976384L; public FindUsageAction(CodeArea codeArea) { - super(NLS.str("popup.find_usage"), codeArea); + super(NLS.str("popup.find_usage") + " (x)", codeArea); + KeyStroke key = getKeyStroke(KeyEvent.VK_X, 0); + codeArea.getInputMap().put(key, "trigger usage"); + codeArea.getActionMap().put("trigger usage", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + node = codeArea.getNodeUnderCaret(); + showUsageDialog(); + } + }); + } + + private void showUsageDialog() { + if (node != null) { + UsageDialog usageDialog = new UsageDialog(codeArea.getMainWindow(), node); + usageDialog.setVisible(true); + node = null; + } } @Override public void actionPerformed(ActionEvent e) { - if (node == null) { - return; - } - UsageDialog usageDialog = new UsageDialog(codeArea.getMainWindow(), node); - usageDialog.setVisible(true); + showUsageDialog(); } @Nullable diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java index 2e19d0590bc615690f6169d1db3afbd61708832d..61959c37da09b558d8f1fa5e204901bd7ebc4aee 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java @@ -1,26 +1,45 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.*; import org.jetbrains.annotations.Nullable; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; +import static javax.swing.KeyStroke.getKeyStroke; + public final class GoToDeclarationAction extends JNodeMenuAction { private static final long serialVersionUID = -1186470538894941301L; public GoToDeclarationAction(CodeArea codeArea) { - super(NLS.str("popup.go_to_declaration"), codeArea); + super(NLS.str("popup.go_to_declaration") + " (d)", codeArea); + KeyStroke key = getKeyStroke(KeyEvent.VK_D, 0); + codeArea.getInputMap().put(key, "trigger goto decl"); + codeArea.getActionMap().put("trigger goto decl", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + node = getNodeByOffset(codeArea.getWordStart(codeArea.getCaretPosition())); + doJump(); + } + }); } - @Override - public void actionPerformed(ActionEvent e) { + private void doJump() { if (node != null) { codeArea.getContentPanel().getTabbedPane().codeJump(node); + node = null; } } + @Override + public void actionPerformed(ActionEvent e) { + doJump(); + } + @Nullable @Override public JumpPosition getNodeByOffset(int offset) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java index d4fcccbe6cd3dc1174523cc16bf517453fa58c03..98d6cb8012af00255db9b528854389067704f635 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java @@ -2,6 +2,7 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; +import javax.swing.*; import javax.swing.event.PopupMenuEvent; import org.jetbrains.annotations.Nullable; @@ -11,6 +12,10 @@ import org.slf4j.LoggerFactory; import jadx.gui.treemodel.JNode; import jadx.gui.ui.RenameDialog; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; + +import static java.awt.event.KeyEvent.VK_N; +import static javax.swing.KeyStroke.getKeyStroke; public final class RenameAction extends JNodeMenuAction { private static final long serialVersionUID = -4680872086148463289L; @@ -18,7 +23,32 @@ public final class RenameAction extends JNodeMenuAction { private static final Logger LOG = LoggerFactory.getLogger(RenameAction.class); public RenameAction(CodeArea codeArea) { - super(NLS.str("popup.rename"), codeArea); + super(NLS.str("popup.rename") + " (n)", codeArea); + KeyStroke key = getKeyStroke(VK_N, 0); + codeArea.getInputMap().put(key, "trigger rename"); + codeArea.getActionMap().put("trigger rename", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + node = codeArea.getNodeUnderCaret(); + showRenameDialog(); + } + }); + } + + private void showRenameDialog() { + if (node == null) { + LOG.info("node == null!"); + UiUtils.showMessageBox(codeArea.getMainWindow(), NLS.str("msg.rename_node_disabled")); + return; + } + if (!node.canRename()) { + UiUtils.showMessageBox(codeArea.getMainWindow(), + NLS.str("msg.rename_node_failed", node.getJavaNode().getFullName())); + LOG.info("node can't be renamed"); + return; + } + RenameDialog.rename(codeArea.getMainWindow(), node); + node = null; } @Override @@ -29,11 +59,7 @@ public final class RenameAction extends JNodeMenuAction { @Override public void actionPerformed(ActionEvent e) { - if (node == null) { - LOG.info("node == null!"); - return; - } - RenameDialog.rename(codeArea.getMainWindow(), node); + showRenameDialog(); } @Nullable diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java index 58a171cb10effd832dc2c8941c4d041799b9841f..bb9dd7415b96e231a75b22bceba4d56b8b7bfb44 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java @@ -5,10 +5,35 @@ import jadx.gui.treemodel.JNode; public class JumpPosition { private final JNode node; private final int line; + // the position of the node in java code, + // call codeArea.scrollToPos(pos) to set caret + private int pos; + // Precise means caret can be set right at the node in codeArea, + // not just the start of the line. + private boolean precise; public JumpPosition(JNode node, int line) { + this(node, line, 0); + } + + public JumpPosition(JNode node, int line, int pos) { this.node = node; this.line = line; + this.pos = pos; + } + + public boolean isPrecise() { + return precise; + } + + public JumpPosition setPrecise(int pos) { + this.pos = pos; + this.precise = true; + return this; + } + + public int getPos() { + return pos; } public JNode getNode() { @@ -28,7 +53,7 @@ public class JumpPosition { return false; } JumpPosition position = (JumpPosition) obj; - return line == position.line && node.equals(position.node); + return line == position.line && node.equals(position.node) && pos == position.pos; } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 31194cbdb816c084be819f9db6cb627a308e1d10..93ad71162ea72050f9a4c57eab7617a8538684ff 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -1,21 +1,16 @@ package jadx.gui.utils; -import java.awt.Image; -import java.awt.Toolkit; -import java.awt.Window; +import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; import java.net.URL; import java.util.ArrayList; import java.util.List; -import javax.swing.Action; -import javax.swing.Icon; -import javax.swing.ImageIcon; -import javax.swing.JComponent; -import javax.swing.KeyStroke; +import javax.swing.*; import org.intellij.lang.annotations.MagicConstant; import org.slf4j.Logger; @@ -206,4 +201,13 @@ public class UiUtils { public static int ctrlButton() { return CTRL_BNT_KEY; } + + public static void showMessageBox(Component parent, String msg) { + JOptionPane.showMessageDialog(parent, msg); + } + + public static void addEscapeShortCutToDispose(JDialog dialog) { + KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 06c0a9ef2682f14484769ccb9fb8972260ba98ca..b0c2048af8664194dd18e32bf9377857632b3367 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -145,7 +145,10 @@ msg.rename_disabled=Einige der Umbenennungseinstellungen sind deaktiviert, bitte msg.rename_disabled_force_rewrite_enabled=Deaktivieren Sie zum Umbenennen die Option "Deobfuscationskartendatei umschreiben erzwingen". msg.rename_disabled_deobfuscation_disabled=Bitte aktivieren Sie die Umbenennung von Deobfuscation. msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht. +#msg.rename_node_disabled= +#msg.rename_node_failed= +#popup.line_wrap= popup.undo=Rückgängig popup.redo=Wiederholen popup.cut=Ausschneiden 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 8cee0d43bbf2bfc5aee2da57ff033a63a83a71c7..c6687adb56fe270dd422f1bc0206efa6dbc527d5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -145,7 +145,10 @@ msg.rename_disabled=Some of rename settings are disabled, next options will be c msg.rename_disabled_force_rewrite_enabled=Disable "Force rewrite deobfuscation map file" option. msg.rename_disabled_deobfuscation_disabled=Enable deobfuscation. msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. +msg.rename_node_disabled=Can't rename this node +msg.rename_node_failed=Can't rename %s +popup.line_wrap=Line Wrap popup.undo=Undo popup.redo=Redo popup.cut=Cut diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 2fed2b09102e6825224d32e605738f4be978b7a3..963e9ff6661e16668b960f752acc11780abea2fe 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -145,7 +145,10 @@ msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivar #msg.rename_disabled_force_rewrite_enabled= #msg.rename_disabled_deobfuscation_disabled= #msg.cmd_select_class_error= +#msg.rename_node_disabled= +#msg.rename_node_failed= +#popup.line_wrap= popup.undo=Deshacer popup.redo=Rehacer popup.cut=Cortar diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 00acab2c9250a2b0ca64cf50647af5f839780861..f5b97587484d5379ea8832cd1b492d473ef8445d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -145,7 +145,10 @@ msg.rename_disabled=일부 이름 바꾸기 설정이 비활성화되고 다음 msg.rename_disabled_force_rewrite_enabled="난독 해제 맵 파일 강제 다시 쓰기"옵션을 비활성화합니다. msg.rename_disabled_deobfuscation_disabled=난독 해제 활성화 msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다. +#msg.rename_node_disabled= +#msg.rename_node_failed= +#popup.line_wrap= popup.undo=실행 취소 popup.redo=다시 실행 popup.cut=자르기 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index e9461c4bbaabeb392261d2c9affe8867b0d50a49..539c1966f5842c744ef4ca11737abbeb38a09ebb 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -145,7 +145,10 @@ msg.rename_disabled=某些重命名设置已禁用,请将此考虑在内 msg.rename_disabled_force_rewrite_enabled=请禁用“强制覆盖反重构映射文件”选项以重命名。 msg.rename_disabled_deobfuscation_disabled=请启用反混淆以重命名。 msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 +#msg.rename_node_disabled= +#msg.rename_node_failed= +#popup.line_wrap= popup.undo=撤销 popup.redo=重做 popup.cut=剪切