From 68b84ea78626ba00e6974b9a1807727f8c3d0b0b Mon Sep 17 00:00:00 2001 From: Mino Date: Thu, 3 Aug 2023 20:05:25 +0100 Subject: [PATCH] feat(gui): allow user to set custom shortcuts (#1479)(PR #1980) * feat(gui): allow user to customize shortcuts * internal: fixed other constructor for jadx action * make code area actions customizable * show warning dialog when mouse button is commonly used * applied code formatting * code formatting and and moved string to resources * moved action related classes to their own package * added fix for actions with modifiers in macos * ignore left click in shortcut edit * applied code formatting * warn user when a duplicate shortcut is entered * save shortcut when key is pressed (instead of typed) * fix node under mouse being ignored Co-authored-by: skylot <118523+skylot@users.noreply.github.com> * add missing import * applied code formatting * added custom shortcuts support to script content panel * save shortcut when key is released (instead of pressed) * enable custom shortcut in script autocomplete * fix duplicate shortcut warning when the shortcut is set again at the same action * fixed mouse buttons shortcut not working for code area * fix exception with mouse button shortcuts * fix action getting fired twice * added variants for forward and back nav actions * fix exception when shortcut is not saved * fix mouse button shortcut for auto complete action * consume mouse event if bound to an action * workaround not being able to extend HashMap * fix exception in script code area when using mouse button shortcut * minor pref serialiazation improvement * fix action buttons not working (like run action) * fix exception with plugin actinos * fixed nullptr when adding an action with null actionmodel to jadxmenu * fix plugin action name not showing --------- Co-authored-by: skylot <118523+skylot@users.noreply.github.com> --- .../gui/plugins/context/CodePopupAction.java | 4 +- .../gui/plugins/script/ScriptCodeArea.java | 14 +- .../plugins/script/ScriptContentPanel.java | 18 +- .../java/jadx/gui/settings/JadxSettings.java | 15 + .../gui/settings/data/ShortcutsWrapper.java | 22 + .../gui/settings/ui/JadxSettingsWindow.java | 3 + .../settings/ui/shortcut/ShortcutEdit.java | 193 ++++++++ .../ui/shortcut/ShortcutsSettingsGroup.java | 60 +++ .../src/main/java/jadx/gui/ui/MainWindow.java | 434 ++++++------------ .../jadx/gui/ui/action/ActionCategory.java | 22 + .../java/jadx/gui/ui/action/ActionModel.java | 154 +++++++ .../jadx/gui/ui/action/IShortcutAction.java | 15 + .../gui/ui/action/JadxAutoCompletion.java | 51 ++ .../jadx/gui/ui/action/JadxGuiAction.java | 141 ++++++ .../java/jadx/gui/ui/codearea/CodeArea.java | 12 +- .../jadx/gui/ui/codearea/CodeAreaAction.java | 24 + .../jadx/gui/ui/codearea/CommentAction.java | 26 +- .../gui/ui/codearea/CommentSearchAction.java | 19 +- .../jadx/gui/ui/codearea/FindUsageAction.java | 9 +- .../jadx/gui/ui/codearea/FridaAction.java | 7 +- .../ui/codearea/GoToDeclarationAction.java | 9 +- .../jadx/gui/ui/codearea/JNodeAction.java | 42 +- .../gui/ui/codearea/JNodePopupBuilder.java | 22 +- .../gui/ui/codearea/JsonPrettifyAction.java | 4 +- .../jadx/gui/ui/codearea/RenameAction.java | 8 +- .../jadx/gui/ui/codearea/XposedAction.java | 7 +- .../java/jadx/gui/ui/menu/HiddenMenuItem.java | 22 + .../main/java/jadx/gui/ui/menu/JadxMenu.java | 64 +++ .../java/jadx/gui/ui/menu/JadxMenuBar.java | 15 + .../jadx/gui/utils/shortcut/Shortcut.java | 150 ++++++ .../utils/shortcut/ShortcutsController.java | 143 ++++++ .../java/jadx/gui/utils/ui/ActionHandler.java | 9 +- .../resources/i18n/Messages_de_DE.properties | 12 + .../resources/i18n/Messages_en_US.properties | 12 + .../resources/i18n/Messages_es_ES.properties | 12 + .../resources/i18n/Messages_ko_KR.properties | 12 + .../resources/i18n/Messages_pt_BR.properties | 12 + .../resources/i18n/Messages_ru_RU.properties | 12 + .../resources/i18n/Messages_zh_CN.properties | 12 + .../resources/i18n/Messages_zh_TW.properties | 12 + 40 files changed, 1434 insertions(+), 400 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/settings/data/ShortcutsWrapper.java create mode 100644 jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java create mode 100644 jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutsSettingsGroup.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/IShortcutAction.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/JadxAutoCompletion.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/JadxGuiAction.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/menu/HiddenMenuItem.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenu.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenuBar.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/shortcut/Shortcut.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/shortcut/ShortcutsController.java diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java index 0551e62db..ed6de8137 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java @@ -34,12 +34,14 @@ public class CodePopupAction { public NodeAction(CodePopupAction data, CodeArea codeArea) { super(data.name, codeArea); + setName(data.name); + setShortcutComponent(codeArea); if (data.keyBinding != null) { KeyStroke key = KeyStroke.getKeyStroke(data.keyBinding); if (key == null) { throw new IllegalArgumentException("Failed to parse key stroke: " + data.keyBinding); } - addKeyBinding(key, data.name); + setKeyBinding(key); } this.data = data; } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java index f05c72db2..d7af18ae3 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java @@ -1,23 +1,21 @@ package jadx.gui.plugins.script; -import java.awt.event.KeyEvent; - -import javax.swing.KeyStroke; - import org.fife.ui.autocomplete.AutoCompletion; import org.jetbrains.annotations.NotNull; import jadx.api.ICodeInfo; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JInputScript; +import jadx.gui.ui.action.JadxAutoCompletion; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.panel.ContentPanel; -import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.ShortcutsController; public class ScriptCodeArea extends AbstractCodeArea { private final JInputScript scriptNode; private final AutoCompletion autoCompletion; + private final ShortcutsController shortcutsController; public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) { super(contentPanel, node); @@ -27,6 +25,7 @@ public class ScriptCodeArea extends AbstractCodeArea { setCodeFoldingEnabled(true); setCloseCurlyBraces(true); + shortcutsController = contentPanel.getTabbedPane().getMainWindow().getShortcutsController(); JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings(); autoCompletion = addAutoComplete(settings); } @@ -34,12 +33,12 @@ public class ScriptCodeArea extends AbstractCodeArea { private AutoCompletion addAutoComplete(JadxSettings settings) { ScriptCompleteProvider provider = new ScriptCompleteProvider(this); provider.setAutoActivationRules(false, "."); - AutoCompletion ac = new AutoCompletion(provider); + JadxAutoCompletion ac = new JadxAutoCompletion(provider); ac.setListCellRenderer(new ScriptCompletionRenderer(settings)); - ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, UiUtils.ctrlButton())); ac.setAutoActivationEnabled(true); ac.setAutoCompleteSingleChoices(true); ac.install(this); + shortcutsController.bindImmediate(ac); return ac; } @@ -80,6 +79,7 @@ public class ScriptCodeArea extends AbstractCodeArea { @Override public void dispose() { + shortcutsController.unbindActionsForComponent(this); autoCompletion.uninstall(); super.dispose(); } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java index 722697f62..e32dbd6b6 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -29,13 +29,14 @@ import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.JInputScript; import jadx.gui.ui.MainWindow; import jadx.gui.ui.TabbedPane; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.AbstractCodeContentPanel; import jadx.gui.ui.codearea.SearchBar; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.NodeLabel; import jadx.plugins.script.ide.ScriptAnalyzeResult; import jadx.plugins.script.ide.ScriptServices; @@ -89,15 +90,14 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { } private JPanel buildScriptActionsPanel() { - ActionHandler runAction = new ActionHandler(this::runScript); - runAction.setNameAndDesc(NLS.str("script.run")); - runAction.setIcon(Icons.RUN); - runAction.attachKeyBindingFor(scriptArea, KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0)); + JadxGuiAction runAction = new JadxGuiAction(ActionModel.SCRIPT_RUN, this::runScript); + JadxGuiAction saveAction = new JadxGuiAction(ActionModel.SCRIPT_SAVE, scriptArea::save); - ActionHandler saveAction = new ActionHandler(scriptArea::save); - saveAction.setNameAndDesc(NLS.str("script.save")); - saveAction.setIcon(Icons.SAVE_ALL); - saveAction.setKeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_S, UiUtils.ctrlButton())); + runAction.setShortcutComponent(scriptArea); + saveAction.setShortcutComponent(scriptArea); + + tabbedPane.getMainWindow().getShortcutsController().bindImmediate(runAction); + tabbedPane.getMainWindow().getShortcutsController().bindImmediate(saveAction); JButton save = saveAction.makeButton(); scriptArea.getScriptNode().addChangeListener(save::setEnabled); 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 1ab3e5233..610f4f2bc 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -37,12 +37,15 @@ import jadx.cli.JadxCLIArgs; import jadx.cli.LogHelper; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.usage.UsageCacheMode; +import jadx.gui.settings.data.ShortcutsWrapper; import jadx.gui.ui.MainWindow; +import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.codearea.EditorTheme; import jadx.gui.utils.FontUtils; import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; +import jadx.gui.utils.shortcut.Shortcut; public class JadxSettings extends JadxCLIArgs { private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class); @@ -73,6 +76,10 @@ public class JadxSettings extends JadxCLIArgs { private boolean autoStartJobs = false; private String excludedPackages = ""; private boolean autoSaveProject = true; + private Map shortcuts = new HashMap<>(); + + @JadxSettingsAdapter.GsonExclude + private ShortcutsWrapper shortcutsWrapper = null; private boolean showHeapUsageBar = false; private boolean alwaysSelectOpened = false; @@ -442,6 +449,14 @@ public class JadxSettings extends JadxCLIArgs { this.autoSaveProject = autoSaveProject; } + public ShortcutsWrapper getShortcuts() { + if (shortcutsWrapper == null) { + shortcutsWrapper = new ShortcutsWrapper(); + shortcutsWrapper.updateShortcuts(shortcuts); + } + return shortcutsWrapper; + } + public void setExportAsGradleProject(boolean exportAsGradleProject) { this.exportAsGradleProject = exportAsGradleProject; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/data/ShortcutsWrapper.java b/jadx-gui/src/main/java/jadx/gui/settings/data/ShortcutsWrapper.java new file mode 100644 index 000000000..4abb53bed --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/data/ShortcutsWrapper.java @@ -0,0 +1,22 @@ +package jadx.gui.settings.data; + +import java.util.Map; + +import jadx.gui.ui.action.ActionModel; +import jadx.gui.utils.shortcut.Shortcut; + +public class ShortcutsWrapper { + private Map shortcuts; + + public void updateShortcuts(Map shortcuts) { + this.shortcuts = shortcuts; + } + + public Shortcut get(ActionModel actionModel) { + return shortcuts.getOrDefault(actionModel, actionModel.getDefaultShortcut()); + } + + public void put(ActionModel actionModel, Shortcut shortcut) { + shortcuts.put(actionModel, shortcut); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java index f874340a8..44d40d053 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java @@ -62,6 +62,7 @@ import jadx.gui.settings.JadxSettingsAdapter; import jadx.gui.settings.LineNumbersMode; import jadx.gui.settings.ui.cache.CacheSettingsGroup; import jadx.gui.settings.ui.plugins.PluginsSettings; +import jadx.gui.settings.ui.shortcut.ShortcutsSettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorTheme; import jadx.gui.utils.FontUtils; @@ -126,6 +127,7 @@ public class JadxSettingsWindow extends JDialog { groups.add(makeRenameGroup()); groups.add(new CacheSettingsGroup(this)); groups.add(makeAppearanceGroup()); + groups.add(new ShortcutsSettingsGroup(this, settings)); groups.add(makeSearchResGroup()); groups.add(makeProjectGroup()); groups.add(new PluginsSettings(mainWindow, settings).build()); @@ -640,6 +642,7 @@ public class JadxSettingsWindow extends JDialog { enableComponents(this, false); SwingUtilities.invokeLater(() -> { if (shouldReload()) { + mainWindow.getShortcutsController().loadSettings(); mainWindow.reopen(); } if (!settings.getLangLocale().equals(prevLang)) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java new file mode 100644 index 000000000..a7e600aaf --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java @@ -0,0 +1,193 @@ +package jadx.gui.settings.ui.shortcut; + +import java.awt.AWTEvent; +import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; + +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.UIManager; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.settings.ui.JadxSettingsWindow; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.Shortcut; + +public class ShortcutEdit extends JPanel { + private static final Icon CLEAR_ICON = UiUtils.openSvgIcon("ui/close"); + + private final ActionModel actionModel; + private final JadxSettingsWindow settingsWindow; + private final JadxSettings settings; + private final TextField textField; + + public Shortcut shortcut; + + public ShortcutEdit(ActionModel actionModel, JadxSettingsWindow settingsWindow, JadxSettings settings) { + this.actionModel = actionModel; + this.settings = settings; + this.settingsWindow = settingsWindow; + + textField = new TextField(); + JButton clearButton = new JButton(CLEAR_ICON); + + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + add(textField); + add(clearButton); + + clearButton.addActionListener(e -> { + setShortcut(Shortcut.none()); + saveShortcut(); + }); + } + + public void setShortcut(Shortcut shortcut) { + this.shortcut = shortcut; + textField.reload(); + } + + private void saveShortcut() { + settings.getShortcuts().put(actionModel, shortcut); + settingsWindow.needReload(); + } + + private boolean verifyShortcut(Shortcut shortcut) { + ActionModel otherAction = null; + for (ActionModel a : ActionModel.values()) { + if (actionModel != a && shortcut.equals(settings.getShortcuts().get(a))) { + otherAction = a; + break; + } + } + + if (otherAction != null) { + int dialogResult = JOptionPane.showConfirmDialog( + this, + NLS.str("msg.duplicate_shortcut", + shortcut, + otherAction.getName(), + otherAction.getCategory().getName()), + NLS.str("msg.warning_title"), + JOptionPane.YES_NO_OPTION); + if (dialogResult != 0) { + return false; + } + } + + return true; + } + + private class TextField extends JTextField { + private Shortcut tempShortcut; + + public TextField() { + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ev -> { + if (!isListening()) { + return false; + } + + if (ev.getID() == KeyEvent.KEY_PRESSED) { + Shortcut pressedShortcut = Shortcut.keyboard(ev.getKeyCode(), ev.getModifiersEx()); + if (pressedShortcut.isValidKeyboard()) { + tempShortcut = pressedShortcut; + refresh(tempShortcut); + } else { + tempShortcut = null; + } + } else if (ev.getID() == KeyEvent.KEY_RELEASED) { + removeFocus(); + } + ev.consume(); + return true; + }); + + addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent ev) { + } + + @Override + public void focusLost(FocusEvent ev) { + if (tempShortcut != null) { + if (verifyShortcut(tempShortcut)) { + shortcut = tempShortcut; + saveShortcut(); + } else { + reload(); + } + tempShortcut = null; + } + } + }); + + Toolkit.getDefaultToolkit().addAWTEventListener(event -> { + if (!isListening()) { + return; + } + + if (event instanceof MouseEvent) { + MouseEvent mouseEvent = (MouseEvent) event; + if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { + int mouseButton = mouseEvent.getButton(); + + if (mouseButton <= MouseEvent.BUTTON1) { + return; + } + + if (mouseButton <= MouseEvent.BUTTON3) { + int dialogResult = JOptionPane.showConfirmDialog( + this, + NLS.str("msg.common_mouse_shortcut"), + NLS.str("msg.warning_title"), + JOptionPane.YES_NO_OPTION); + if (dialogResult != 0) { + ((MouseEvent) event).consume(); + tempShortcut = null; + removeFocus(); + return; + } + } + + ((MouseEvent) event).consume(); + tempShortcut = Shortcut.mouse(mouseButton); + refresh(tempShortcut); + removeFocus(); + } + } + }, AWTEvent.MOUSE_EVENT_MASK); + } + + public void reload() { + refresh(shortcut); + } + + private void refresh(Shortcut displayedShortcut) { + if (displayedShortcut == null || displayedShortcut.isNone()) { + setText("None"); + setForeground(UIManager.getColor("TextArea.inactiveForeground")); + return; + } + setText(displayedShortcut.toString()); + setForeground(UIManager.getColor("TextArea.foreground")); + } + + private void removeFocus() { + // triggers focusLost + getRootPane().requestFocus(); + } + + private boolean isListening() { + return isFocusOwner(); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutsSettingsGroup.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutsSettingsGroup.java new file mode 100644 index 000000000..8aafb8da7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutsSettingsGroup.java @@ -0,0 +1,60 @@ +package jadx.gui.settings.ui.shortcut; + +import java.awt.BorderLayout; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import jadx.api.plugins.gui.ISettingsGroup; +import jadx.gui.settings.JadxSettings; +import jadx.gui.settings.ui.JadxSettingsWindow; +import jadx.gui.settings.ui.SettingsGroup; +import jadx.gui.ui.action.ActionCategory; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.utils.NLS; +import jadx.gui.utils.shortcut.Shortcut; + +public class ShortcutsSettingsGroup implements ISettingsGroup { + private final JadxSettingsWindow settingsWindow; + private final JadxSettings settings; + + public ShortcutsSettingsGroup(JadxSettingsWindow settingsWindow, JadxSettings settings) { + this.settingsWindow = settingsWindow; + this.settings = settings; + } + + @Override + public String getTitle() { + return NLS.str("preferences.shortcuts"); + } + + @Override + public JComponent buildComponent() { + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(new JLabel(NLS.str("preferences.select_shortcuts")), BorderLayout.NORTH); + return panel; + } + + @Override + public List getSubGroups() { + return Arrays.stream(ActionCategory.values()) + .map(this::makeShortcutsGroup) + .collect(Collectors.toUnmodifiableList()); + } + + private SettingsGroup makeShortcutsGroup(ActionCategory category) { + SettingsGroup group = new SettingsGroup(category.getName()); + for (ActionModel actionModel : ActionModel.select(category)) { + Shortcut shortcut = settings.getShortcuts().get(actionModel); + ShortcutEdit edit = new ShortcutEdit(actionModel, settingsWindow, settings); + edit.setShortcut(shortcut); + group.addRow(actionModel.getName(), edit); + } + return group; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 216d7c5d1..79b5c3cbd 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -1,6 +1,5 @@ package jadx.gui.ui; -import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; @@ -9,14 +8,12 @@ import java.awt.Font; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; -import java.awt.Toolkit; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; -import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; @@ -45,10 +42,10 @@ import javax.swing.Action; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; +import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenu; -import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; @@ -118,6 +115,8 @@ import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JRoot; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.AbstractCodeContentPanel; import jadx.gui.ui.codearea.EditorTheme; @@ -128,6 +127,9 @@ import jadx.gui.ui.dialog.LogViewerDialog; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; +import jadx.gui.ui.menu.HiddenMenuItem; +import jadx.gui.ui.menu.JadxMenu; +import jadx.gui.ui.menu.JadxMenuBar; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.IssuesPanel; import jadx.gui.ui.panel.JDebuggerPanel; @@ -141,18 +143,16 @@ import jadx.gui.update.data.Release; import jadx.gui.utils.CacheObject; import jadx.gui.utils.FontUtils; import jadx.gui.utils.ILoadListener; -import jadx.gui.utils.Icons; import jadx.gui.utils.LafManager; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; -import jadx.gui.utils.SystemInfo; import jadx.gui.utils.UiUtils; import jadx.gui.utils.fileswatcher.LiveReloadWorker; +import jadx.gui.utils.shortcut.ShortcutsController; import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.NodeLabel; import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE; -import static javax.swing.KeyStroke.getKeyStroke; public class MainWindow extends JFrame { private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class); @@ -191,8 +191,8 @@ public class MainWindow extends JFrame { private transient @NotNull JadxProject project; - private transient Action newProjectAction; - private transient Action saveProjectAction; + private transient JadxGuiAction newProjectAction; + private transient JadxGuiAction saveProjectAction; private transient JPanel mainPanel; private transient JSplitPane treeSplitPane; @@ -227,7 +227,10 @@ public class MainWindow extends JFrame { private final List loadListeners = new ArrayList<>(); private final List> treeUpdateListener = new ArrayList<>(); private boolean loaded; + private boolean settingsOpen = false; + private ShortcutsController shortcutsController; + private JadxMenuBar menuBar; private JMenu pluginsMenu; private final transient RenameMappingsGui renameMappings; @@ -240,6 +243,7 @@ public class MainWindow extends JFrame { this.liveReloadWorker = new LiveReloadWorker(this); this.renameMappings = new RenameMappingsGui(this); this.cacheManager = new CacheManager(settings); + this.shortcutsController = new ShortcutsController(settings); resetCache(); FontUtils.registerBundledFonts(); @@ -247,8 +251,8 @@ public class MainWindow extends JFrame { initUI(); this.backgroundExecutor = new BackgroundExecutor(settings, progressPane); initMenuAndToolbar(); - registerMouseNavigationButtons(); UiUtils.setWindowIcons(this); + shortcutsController.registerMouseEventListener(this); loadSettings(); update(); @@ -472,6 +476,8 @@ public class MainWindow extends JFrame { saveAll(); closeAll(); loadFiles(EMPTY_RUNNABLE); + + menuBar.reloadShortcuts(); } private void openProject(Path path, Runnable onFinish) { @@ -865,99 +871,75 @@ public class MainWindow extends JFrame { tree.requestFocus(); } + public void textSearch() { + ContentPanel panel = tabbedPane.getSelectedContentPanel(); + if (panel instanceof AbstractCodeContentPanel) { + AbstractCodeArea codeArea = ((AbstractCodeContentPanel) panel).getCodeArea(); + String preferText = codeArea.getSelectedText(); + if (StringUtils.isEmpty(preferText)) { + preferText = codeArea.getWordUnderCaret(); + } + if (!StringUtils.isEmpty(preferText)) { + SearchDialog.searchText(MainWindow.this, preferText); + return; + } + } + SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT); + } + + public void gotoMainActivity() { + AndroidManifestParser parser = new AndroidManifestParser( + AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), + EnumSet.of(AppAttribute.MAIN_ACTIVITY)); + if (!parser.isManifestFound()) { + JOptionPane.showMessageDialog(MainWindow.this, + NLS.str("error_dialog.not_found", "AndroidManifest.xml"), + NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + return; + } + try { + ApplicationParams results = parser.parse(); + if (results.getMainActivityName() == null) { + throw new JadxRuntimeException("Failed to get main activity name from manifest"); + } + JavaClass mainActivityClass = results.getMainActivity(getWrapper().getDecompiler()); + if (mainActivityClass == null) { + throw new JadxRuntimeException("Failed to find main activity class: " + results.getMainActivityName()); + } + tabbedPane.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass)); + } catch (Exception e) { + LOG.error("Main activity not found", e); + JOptionPane.showMessageDialog(MainWindow.this, + NLS.str("error_dialog.not_found", "Main Activity"), + NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + private void initMenuAndToolbar() { - ActionHandler openAction = new ActionHandler(this::openFileDialog); - openAction.setNameAndDesc(NLS.str("file.open_action")); - openAction.setIcon(Icons.OPEN); - openAction.setKeyBinding(getKeyStroke(KeyEvent.VK_O, UiUtils.ctrlButton())); + JadxGuiAction openAction = new JadxGuiAction(ActionModel.OPEN, this::openFileDialog); + JadxGuiAction openProject = new JadxGuiAction(ActionModel.OPEN_PROJECT, this::openProjectDialog); - ActionHandler openProject = new ActionHandler(this::openProjectDialog); - openProject.setNameAndDesc(NLS.str("file.open_project")); - openProject.setIcon(Icons.OPEN_PROJECT); - openProject.setKeyBinding(getKeyStroke(KeyEvent.VK_O, InputEvent.SHIFT_DOWN_MASK | UiUtils.ctrlButton())); + JadxGuiAction addFilesAction = new JadxGuiAction(ActionModel.ADD_FILES, () -> addFiles()); + newProjectAction = new JadxGuiAction(ActionModel.NEW_PROJECT, this::newProject); + saveProjectAction = new JadxGuiAction(ActionModel.SAVE_PROJECT, this::saveProject); + JadxGuiAction saveProjectAsAction = new JadxGuiAction(ActionModel.SAVE_PROJECT_AS, this::saveProjectAs); + JadxGuiAction reloadAction = new JadxGuiAction(ActionModel.RELOAD, () -> UiUtils.uiRun(this::reopen)); + JadxGuiAction liveReloadAction = new JadxGuiAction(ActionModel.LIVE_RELOAD, + () -> updateLiveReload(!project.isEnableLiveReload())); - Action addFilesAction = new AbstractAction(NLS.str("file.add_files_action"), ICON_ADD_FILES) { - @Override - public void actionPerformed(ActionEvent e) { - addFiles(); - } - }; - addFilesAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.add_files_action")); - - newProjectAction = new AbstractAction(NLS.str("file.new_project"), Icons.NEW_PROJECT) { - @Override - public void actionPerformed(ActionEvent e) { - newProject(); - } - }; - newProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.new_project")); - - saveProjectAction = new AbstractAction(NLS.str("file.save_project")) { - @Override - public void actionPerformed(ActionEvent e) { - saveProject(); - } - }; - saveProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project")); - - Action saveProjectAsAction = new AbstractAction(NLS.str("file.save_project_as")) { - @Override - public void actionPerformed(ActionEvent e) { - saveProjectAs(); - } - }; - saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as")); - - ActionHandler reload = new ActionHandler(ev -> UiUtils.uiRun(this::reopen)); - reload.setNameAndDesc(NLS.str("file.reload")); - reload.setIcon(ICON_RELOAD); - reload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, 0)); - - ActionHandler liveReload = new ActionHandler(ev -> updateLiveReload(!project.isEnableLiveReload())); - liveReload.setName(NLS.str("file.live_reload")); - liveReload.setShortDescription(NLS.str("file.live_reload_desc")); - liveReload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK)); - - liveReloadMenuItem = new JCheckBoxMenuItem(liveReload); + liveReloadMenuItem = new JCheckBoxMenuItem(liveReloadAction); liveReloadMenuItem.setState(project.isEnableLiveReload()); - Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), Icons.SAVE_ALL) { - @Override - public void actionPerformed(ActionEvent e) { - saveAll(false); - } - }; - saveAllAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_all")); - saveAllAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton())); + JadxGuiAction saveAllAction = new JadxGuiAction(ActionModel.SAVE_ALL, () -> saveAll(false)); + JadxGuiAction exportAction = new JadxGuiAction(ActionModel.EXPORT, () -> saveAll(true)); - Action exportAction = new AbstractAction(NLS.str("file.export_gradle"), ICON_EXPORT) { - @Override - public void actionPerformed(ActionEvent e) { - saveAll(true); - } - }; - exportAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.export_gradle")); - exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); - - JMenu recentProjects = new JMenu(NLS.str("menu.recent_projects")); + JMenu recentProjects = new JadxMenu(NLS.str("menu.recent_projects"), shortcutsController); recentProjects.addMenuListener(new RecentProjectsMenuListener(this, recentProjects)); - Action prefsAction = new AbstractAction(NLS.str("menu.preferences"), ICON_PREF) { - @Override - public void actionPerformed(ActionEvent e) { - new JadxSettingsWindow(MainWindow.this, settings).setVisible(true); - } - }; - prefsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.preferences")); - prefsAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_P, - UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); - - Action exitAction = new AbstractAction(NLS.str("file.exit"), ICON_EXIT) { - @Override - public void actionPerformed(ActionEvent e) { - closeWindow(); - } - }; + JadxGuiAction prefsAction = new JadxGuiAction(ActionModel.PREFS, this::openSettings); + JadxGuiAction exitAction = new JadxGuiAction(ActionModel.EXIT, this::closeWindow); isFlattenPackage = settings.isFlattenPackage(); flatPkgMenuItem = new JCheckBoxMenuItem(NLS.str("menu.flatten"), ICON_FLAT_PKG); @@ -983,109 +965,17 @@ public class MainWindow extends JFrame { dockLog.setState(settings.isDockLogViewer()); dockLog.addActionListener(event -> settings.setDockLogViewer(!settings.isDockLogViewer())); - Action syncAction = new AbstractAction(NLS.str("menu.sync"), ICON_SYNC) { - @Override - public void actionPerformed(ActionEvent e) { - syncWithEditor(); - } - }; - syncAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.sync")); - syncAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_T, UiUtils.ctrlButton())); - - Action textSearchAction = new AbstractAction(NLS.str("menu.text_search"), ICON_SEARCH) { - @Override - public void actionPerformed(ActionEvent e) { - ContentPanel panel = tabbedPane.getSelectedContentPanel(); - if (panel instanceof AbstractCodeContentPanel) { - AbstractCodeArea codeArea = ((AbstractCodeContentPanel) panel).getCodeArea(); - String preferText = codeArea.getSelectedText(); - if (StringUtils.isEmpty(preferText)) { - preferText = codeArea.getWordUnderCaret(); - } - if (!StringUtils.isEmpty(preferText)) { - SearchDialog.searchText(MainWindow.this, preferText); - return; - } - } - SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT); - } - }; - textSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.text_search")); - textSearchAction.putValue(Action.ACCELERATOR_KEY, - getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); - - Action clsSearchAction = new AbstractAction(NLS.str("menu.class_search"), ICON_FIND) { - @Override - public void actionPerformed(ActionEvent e) { - SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS); - } - }; - clsSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.class_search")); - clsSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_N, UiUtils.ctrlButton())); - - Action commentSearchAction = new AbstractAction(NLS.str("menu.comment_search"), ICON_COMMENT_SEARCH) { - @Override - public void actionPerformed(ActionEvent e) { - SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.COMMENT); - } - }; - commentSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.comment_search")); - commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON, - UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); - - Action gotoMainActivityAction = new AbstractAction(NLS.str("menu.goto_main_activity"), ICON_MAIN_ACTIVITY) { - @Override - public void actionPerformed(ActionEvent ev) { - AndroidManifestParser parser = new AndroidManifestParser( - AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), - EnumSet.of(AppAttribute.MAIN_ACTIVITY)); - if (!parser.isManifestFound()) { - JOptionPane.showMessageDialog(MainWindow.this, - NLS.str("error_dialog.not_found", "AndroidManifest.xml"), - NLS.str("error_dialog.title"), - JOptionPane.ERROR_MESSAGE); - return; - } - try { - ApplicationParams results = parser.parse(); - if (results.getMainActivityName() == null) { - throw new JadxRuntimeException("Failed to get main activity name from manifest"); - } - JavaClass mainActivityClass = results.getMainActivity(getWrapper().getDecompiler()); - if (mainActivityClass == null) { - throw new JadxRuntimeException("Failed to find main activity class: " + results.getMainActivityName()); - } - tabbedPane.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass)); - } catch (Exception e) { - LOG.error("Main activity not found", e); - JOptionPane.showMessageDialog(MainWindow.this, - NLS.str("error_dialog.not_found", "Main Activity"), - NLS.str("error_dialog.title"), - JOptionPane.ERROR_MESSAGE); - } - } - }; - gotoMainActivityAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.goto_main_activity")); - gotoMainActivityAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_M, - UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); - - ActionHandler decompileAllAction = new ActionHandler(ev -> requestFullDecompilation()); - decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all")); - decompileAllAction.setIcon(ICON_DECOMPILE_ALL); - - ActionHandler resetCacheAction = new ActionHandler(ev -> resetCodeCache()); - resetCacheAction.setNameAndDesc(NLS.str("menu.reset_cache")); - resetCacheAction.setIcon(Icons.RESET); - - Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) { - @Override - public void actionPerformed(ActionEvent e) { - toggleDeobfuscation(); - } - }; - deobfAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("preferences.deobfuscation")); - deobfAction.putValue(Action.ACCELERATOR_KEY, - getKeyStroke(KeyEvent.VK_D, UiUtils.ctrlButton() | KeyEvent.ALT_DOWN_MASK)); + JadxGuiAction syncAction = new JadxGuiAction(ActionModel.SYNC, this::syncWithEditor); + JadxGuiAction textSearchAction = new JadxGuiAction(ActionModel.TEXT_SEARCH, this::textSearch); + JadxGuiAction clsSearchAction = new JadxGuiAction(ActionModel.CLASS_SEARCH, + () -> SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS)); + JadxGuiAction commentSearchAction = new JadxGuiAction(ActionModel.COMMENT_SEARCH, + () -> SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.COMMENT)); + JadxGuiAction gotoMainActivityAction = new JadxGuiAction(ActionModel.GOTO_MAIN_ACTIVITY, + this::gotoMainActivity); + JadxGuiAction decompileAllAction = new JadxGuiAction(ActionModel.DECOMPILE_ALL, this::requestFullDecompilation); + JadxGuiAction resetCacheAction = new JadxGuiAction(ActionModel.RESET_CACHE, this::resetCodeCache); + JadxGuiAction deobfAction = new JadxGuiAction(ActionModel.DEOBF, this::toggleDeobfuscation); deobfToggleBtn = new JToggleButton(deobfAction); deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); @@ -1094,54 +984,19 @@ public class MainWindow extends JFrame { deobfMenuItem = new JCheckBoxMenuItem(deobfAction); deobfMenuItem.setState(settings.isDeobfuscationOn()); - ActionHandler showLog = new ActionHandler(ev -> showLogViewer(LogOptions.current())); - showLog.setNameAndDesc(NLS.str("menu.log")); - showLog.setIcon(ICON_LOG); - showLog.setKeyBinding(getKeyStroke(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); + JadxGuiAction showLogAction = new JadxGuiAction(ActionModel.SHOW_LOG, + () -> showLogViewer(LogOptions.current())); + JadxGuiAction aboutAction = new JadxGuiAction(ActionModel.ABOUT, () -> new AboutDialog().setVisible(true)); + JadxGuiAction backAction = new JadxGuiAction(ActionModel.BACK, tabbedPane::navBack); + JadxGuiAction backVariantAction = new JadxGuiAction(ActionModel.BACK_V, tabbedPane::navBack); + JadxGuiAction forwardAction = new JadxGuiAction(ActionModel.FORWARD, tabbedPane::navForward); + JadxGuiAction forwardVariantAction = new JadxGuiAction(ActionModel.FORWARD_V, tabbedPane::navForward); + JadxGuiAction quarkAction = new JadxGuiAction(ActionModel.QUARK, + () -> new QuarkDialog(MainWindow.this).setVisible(true)); + JadxGuiAction openDeviceAction = new JadxGuiAction(ActionModel.OPEN_DEVICE, + () -> new ADBDialog(MainWindow.this).setVisible(true)); - Action aboutAction = new AbstractAction(NLS.str("menu.about"), ICON_INFO) { - @Override - public void actionPerformed(ActionEvent e) { - new AboutDialog().setVisible(true); - } - }; - - Action backAction = new AbstractAction(NLS.str("nav.back"), ICON_BACK) { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane.navBack(); - } - }; - backAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("nav.back")); - backAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_ESCAPE, 0)); - - Action forwardAction = new AbstractAction(NLS.str("nav.forward"), ICON_FORWARD) { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane.navForward(); - } - }; - forwardAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("nav.forward")); - forwardAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK, SystemInfo.IS_MAC)); - - Action quarkAction = new AbstractAction("Quark Engine", ICON_QUARK) { - @Override - public void actionPerformed(ActionEvent e) { - new QuarkDialog(MainWindow.this).setVisible(true); - } - }; - quarkAction.putValue(Action.SHORT_DESCRIPTION, "Quark Engine"); - - Action openDeviceAction = new AbstractAction(NLS.str("debugger.process_selector"), ICON_DEBUGGER) { - @Override - public void actionPerformed(ActionEvent e) { - ADBDialog dialog = new ADBDialog(MainWindow.this); - dialog.setVisible(true); - } - }; - openDeviceAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.process_selector")); - - JMenu file = new JMenu(NLS.str("menu.file")); + JMenu file = new JadxMenu(NLS.str("menu.file"), shortcutsController); file.setMnemonic(KeyEvent.VK_F); file.add(openAction); file.add(openProject); @@ -1151,7 +1006,7 @@ public class MainWindow extends JFrame { file.add(saveProjectAction); file.add(saveProjectAsAction); file.addSeparator(); - file.add(reload); + file.add(reloadAction); file.add(liveReloadMenuItem); renameMappings.addMenuActions(file); file.addSeparator(); @@ -1164,7 +1019,7 @@ public class MainWindow extends JFrame { file.addSeparator(); file.add(exitAction); - JMenu view = new JMenu(NLS.str("menu.view")); + JMenu view = new JadxMenu(NLS.str("menu.view"), shortcutsController); view.setMnemonic(KeyEvent.VK_V); view.add(flatPkgMenuItem); view.add(syncAction); @@ -1172,7 +1027,7 @@ public class MainWindow extends JFrame { view.add(alwaysSelectOpened); view.add(dockLog); - JMenu nav = new JMenu(NLS.str("menu.navigation")); + JMenu nav = new JadxMenu(NLS.str("menu.navigation"), shortcutsController); nav.setMnemonic(KeyEvent.VK_N); nav.add(textSearchAction); nav.add(clsSearchAction); @@ -1182,11 +1037,11 @@ public class MainWindow extends JFrame { nav.add(backAction); nav.add(forwardAction); - pluginsMenu = new JMenu(NLS.str("menu.plugins")); + pluginsMenu = new JadxMenu(NLS.str("menu.plugins"), shortcutsController); pluginsMenu.setMnemonic(KeyEvent.VK_P); resetPluginsMenu(); - JMenu tools = new JMenu(NLS.str("menu.tools")); + JMenu tools = new JadxMenu(NLS.str("menu.tools"), shortcutsController); tools.setMnemonic(KeyEvent.VK_T); tools.add(decompileAllAction); tools.add(resetCacheAction); @@ -1194,9 +1049,9 @@ public class MainWindow extends JFrame { tools.add(quarkAction); tools.add(openDeviceAction); - JMenu help = new JMenu(NLS.str("menu.help")); + JMenu help = new JadxMenu(NLS.str("menu.help"), shortcutsController); help.setMnemonic(KeyEvent.VK_H); - help.add(showLog); + help.add(showLogAction); if (Jadx.isDevVersion()) { help.add(new AbstractAction("Show sample error report") { @Override @@ -1207,7 +1062,7 @@ public class MainWindow extends JFrame { } help.add(aboutAction); - JMenuBar menuBar = new JMenuBar(); + menuBar = new JadxMenuBar(); menuBar.add(file); menuBar.add(view); menuBar.add(nav); @@ -1231,7 +1086,7 @@ public class MainWindow extends JFrame { toolbar.add(openAction); toolbar.add(addFilesAction); toolbar.addSeparator(); - toolbar.add(reload); + toolbar.add(reloadAction); toolbar.addSeparator(); toolbar.add(saveAllAction); toolbar.add(exportAction); @@ -1251,7 +1106,7 @@ public class MainWindow extends JFrame { toolbar.add(quarkAction); toolbar.add(openDeviceAction); toolbar.addSeparator(); - toolbar.add(showLog); + toolbar.add(showLogAction); toolbar.addSeparator(); toolbar.add(prefsAction); toolbar.addSeparator(); @@ -1260,18 +1115,26 @@ public class MainWindow extends JFrame { mainPanel.add(toolbar, BorderLayout.NORTH); + nav.add(new HiddenMenuItem(backVariantAction)); + nav.add(new HiddenMenuItem(forwardVariantAction)); + + shortcutsController.bind(backVariantAction); + shortcutsController.bind(forwardVariantAction); + addLoadListener(loaded -> { textSearchAction.setEnabled(loaded); clsSearchAction.setEnabled(loaded); commentSearchAction.setEnabled(loaded); gotoMainActivityAction.setEnabled(loaded); backAction.setEnabled(loaded); + backVariantAction.setEnabled(loaded); forwardAction.setEnabled(loaded); + forwardVariantAction.setEnabled(loaded); syncAction.setEnabled(loaded); saveAllAction.setEnabled(loaded); exportAction.setEnabled(loaded); saveProjectAsAction.setEnabled(loaded); - reload.setEnabled(loaded); + reloadAction.setEnabled(loaded); decompileAllAction.setEnabled(loaded); deobfAction.setEnabled(loaded); quarkAction.setEnabled(loaded); @@ -1402,42 +1265,6 @@ public class MainWindow extends JFrame { setTitle(DEFAULT_TITLE); } - private void registerMouseNavigationButtons() { - Toolkit toolkit = Toolkit.getDefaultToolkit(); - toolkit.addAWTEventListener(event -> { - if (event instanceof MouseEvent) { - MouseEvent mouseEvent = (MouseEvent) event; - if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { - int rawButton = mouseEvent.getButton(); - if (rawButton <= 3) { - return; - } - int button = remapMouseButton(rawButton); - switch (button) { - case 4: - tabbedPane.navBack(); - break; - case 5: - tabbedPane.navForward(); - break; - } - } - } - }, AWTEvent.MOUSE_EVENT_MASK); - } - - private static int remapMouseButton(int rawButton) { - if (SystemInfo.IS_LINUX) { - if (rawButton == 6) { - return 4; - } - if (rawButton == 7) { - return 5; - } - } - return rawButton; - } - private static String[] getPathExpansion(TreePath path) { List pathList = new ArrayList<>(); while (path != null) { @@ -1515,6 +1342,23 @@ public class MainWindow extends JFrame { return editorTheme; } + private void openSettings() { + settingsOpen = true; + + JDialog settingsWindow = new JadxSettingsWindow(MainWindow.this, settings); + settingsWindow.setVisible(true); + settingsWindow.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + settingsOpen = false; + } + }); + } + + public boolean isSettingsOpen() { + return settingsOpen; + } + public void loadSettings() { // queue update to not interrupt current UI tasks UiUtils.uiRun(this::updateUiSettings); @@ -1535,6 +1379,8 @@ public class MainWindow extends JFrame { if (logPanel != null) { logPanel.loadSettings(); } + + shortcutsController.loadSettings(); } private void closeWindow() { @@ -1638,6 +1484,10 @@ public class MainWindow extends JFrame { return debuggerPanel; } + public ShortcutsController getShortcutsController() { + return shortcutsController; + } + public void showDebuggerPanel() { initDebuggerPanel(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java new file mode 100644 index 000000000..c72b32562 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java @@ -0,0 +1,22 @@ +package jadx.gui.ui.action; + +import jadx.gui.utils.NLS; + +public enum ActionCategory { + MENU_TOOLBAR("action_category.menu_toolbar"), + CODE_AREA("action_category.code_area"), + PLUGIN_SCRIPT("action_category.plugin_script"); + + private final String nameRes; + + ActionCategory(String nameRes) { + this.nameRes = nameRes; + } + + public String getName() { + if (nameRes != null) { + return NLS.str(nameRes); + } + return null; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java new file mode 100644 index 000000000..7f8747e8b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java @@ -0,0 +1,154 @@ +package jadx.gui.ui.action; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.ImageIcon; + +import io.reactivex.annotations.NonNull; + +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.Shortcut; + +import static jadx.gui.ui.action.ActionCategory.*; + +public enum ActionModel { + ABOUT(MENU_TOOLBAR, "menu.about", "menu.about", "ui/showInfos", + Shortcut.keyboard(KeyEvent.VK_F1)), + OPEN(MENU_TOOLBAR, "file.open_action", "file.open_action", "ui/openDisk", + Shortcut.keyboard(KeyEvent.VK_O, KeyEvent.CTRL_DOWN_MASK)), + OPEN_PROJECT(MENU_TOOLBAR, "file.open_project", "file.open_project", "ui/projectDirectory", + Shortcut.keyboard(KeyEvent.VK_O, InputEvent.SHIFT_DOWN_MASK | UiUtils.ctrlButton())), + ADD_FILES(MENU_TOOLBAR, "file.add_files_action", "file.add_files_action", "ui/addFile", + Shortcut.none()), + NEW_PROJECT(MENU_TOOLBAR, "file.new_project", "file.new_project", "ui/newFolder", + Shortcut.none()), + SAVE_PROJECT(MENU_TOOLBAR, "file.save_project", "file.save_project", null, + Shortcut.none()), + SAVE_PROJECT_AS(MENU_TOOLBAR, "file.save_project_as", "file.save_project_as", null, + Shortcut.none()), + RELOAD(MENU_TOOLBAR, "file.reload", "file.reload", "ui/refresh", + Shortcut.keyboard(KeyEvent.VK_F5)), + LIVE_RELOAD(MENU_TOOLBAR, "file.live_reload", "file.live_reload_desc", null, + Shortcut.keyboard(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK)), + SAVE_ALL(MENU_TOOLBAR, "file.save_all", "file.save_all", "ui/menu-saveall", + Shortcut.keyboard(KeyEvent.VK_E, UiUtils.ctrlButton())), + EXPORT(MENU_TOOLBAR, "file.export_gradle", "file.export_gradle", "ui/export", + Shortcut.keyboard(KeyEvent.VK_E, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + PREFS(MENU_TOOLBAR, "menu.preferences", "menu.preferences", "ui/settings", + Shortcut.keyboard(KeyEvent.VK_P, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + EXIT(MENU_TOOLBAR, "file.exit", "file.exit", "ui/exit", + Shortcut.none()), + SYNC(MENU_TOOLBAR, "menu.sync", "menu.sync", "ui/pagination", + Shortcut.keyboard(KeyEvent.VK_T, UiUtils.ctrlButton())), + TEXT_SEARCH(MENU_TOOLBAR, "menu.text_search", "menu.text_search", "ui/find", + Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + CLASS_SEARCH(MENU_TOOLBAR, "menu.class_search", "menu.class_search", "ui/ejbFinderMethod", + Shortcut.keyboard(KeyEvent.VK_N, UiUtils.ctrlButton())), + COMMENT_SEARCH(MENU_TOOLBAR, "menu.comment_search", "menu.comment_search", "ui/usagesFinder", + Shortcut.keyboard(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + GOTO_MAIN_ACTIVITY(MENU_TOOLBAR, "menu.goto_main_activity", "menu.goto_main_activity", "ui/home", + Shortcut.keyboard(KeyEvent.VK_M, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + DECOMPILE_ALL(MENU_TOOLBAR, "menu.decompile_all", "menu.decompile_all", "ui/runAll", + Shortcut.none()), + RESET_CACHE(MENU_TOOLBAR, "menu.reset_cache", "menu.reset_cache", "ui/reset", + Shortcut.none()), + DEOBF(MENU_TOOLBAR, "menu.deobfuscation", "preferences.deobfuscation", "ui/helmChartLock", + Shortcut.keyboard(KeyEvent.VK_D, UiUtils.ctrlButton() | KeyEvent.ALT_DOWN_MASK)), + SHOW_LOG(MENU_TOOLBAR, "menu.log", "menu.log", "ui/logVerbose", + Shortcut.keyboard(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + BACK(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", + Shortcut.keyboard(KeyEvent.VK_ESCAPE)), + BACK_V(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", + Shortcut.none()), + FORWARD(MENU_TOOLBAR, "nav.forward", "nav.forward", "ui/right", + Shortcut.keyboard(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK)), + FORWARD_V(MENU_TOOLBAR, "nav.forward", "nav.forward", "ui/right", + Shortcut.none()), + QUARK(MENU_TOOLBAR, "menu.quark", "menu.quark", "ui/quark", + Shortcut.none()), + OPEN_DEVICE(MENU_TOOLBAR, "debugger.process_selector", "debugger.process_selector", "ui/startDebugger", + Shortcut.none()), + + FIND_USAGE(CODE_AREA, "popup.find_usage", "popup.find_usage", null, + Shortcut.keyboard(KeyEvent.VK_X)), + GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", "popup.go_to_declaration", null, + Shortcut.keyboard(KeyEvent.VK_D)), + CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null, + Shortcut.keyboard(KeyEvent.VK_SEMICOLON)), + CODE_COMMENT_SEARCH(CODE_AREA, "popup.search_comment", "popup.search_comment", null, + Shortcut.keyboard(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton())), + CODE_RENAME(CODE_AREA, "popup.rename", "popup.rename", null, + Shortcut.keyboard(KeyEvent.VK_N)), + FRIDA_COPY(CODE_AREA, "popup.frida", "popup.frida", null, + Shortcut.keyboard(KeyEvent.VK_F)), + XPOSED_COPY(CODE_AREA, "popup.xposed", "popup.xposed", null, + Shortcut.keyboard(KeyEvent.VK_Y)), + JSON_PRETTIFY(CODE_AREA, "popup.json_prettify", "popup.json_prettify", null, + Shortcut.none()), + + SCRIPT_RUN(PLUGIN_SCRIPT, "script.run", "script.run", "ui/run", + Shortcut.keyboard(KeyEvent.VK_F8)), + SCRIPT_SAVE(PLUGIN_SCRIPT, "script.save", "script.save", "ui/menu-saveall", + Shortcut.keyboard(KeyEvent.VK_S, UiUtils.ctrlButton())), + SCRIPT_AUTO_COMPLETE(PLUGIN_SCRIPT, "script.auto_complete", "script.auto_complete", null, + Shortcut.keyboard(KeyEvent.VK_SPACE, UiUtils.ctrlButton())); + + private final ActionCategory category; + private final String nameRes; + private final String descRes; + private final String iconPath; + private final Shortcut defaultShortcut; + + ActionModel(ActionCategory category, String nameRes, String descRes, String iconPath, Shortcut defaultShortcut) { + this.category = category; + this.nameRes = nameRes; + this.descRes = descRes; + this.iconPath = iconPath; + this.defaultShortcut = defaultShortcut; + } + + public static List select(ActionCategory category) { + return Arrays.stream(values()) + .filter(actionModel -> actionModel.category == category) + .collect(Collectors.toUnmodifiableList()); + } + + public ActionCategory getCategory() { + return category; + } + + public String getName() { + if (nameRes != null) { + String name = NLS.str(nameRes); + if (name().endsWith("_V")) { + name = NLS.str("action.variant", name); + } + return name; + } + return null; + } + + public String getDescription() { + if (descRes != null) { + return NLS.str(descRes); + } + return null; + } + + public ImageIcon getIcon() { + if (iconPath != null) { + return UiUtils.openSvgIcon(iconPath); + } + return null; + } + + @NonNull + public Shortcut getDefaultShortcut() { + return defaultShortcut; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/IShortcutAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/IShortcutAction.java new file mode 100644 index 000000000..c56934073 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/IShortcutAction.java @@ -0,0 +1,15 @@ +package jadx.gui.ui.action; + +import javax.swing.JComponent; + +import jadx.gui.utils.shortcut.Shortcut; + +public interface IShortcutAction { + ActionModel getActionModel(); + + JComponent getShortcutComponent(); + + void performAction(); + + void setShortcut(Shortcut shortcut); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/JadxAutoCompletion.java b/jadx-gui/src/main/java/jadx/gui/ui/action/JadxAutoCompletion.java new file mode 100644 index 000000000..c698f623f --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/JadxAutoCompletion.java @@ -0,0 +1,51 @@ +package jadx.gui.ui.action; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.JComponent; +import javax.swing.KeyStroke; + +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.autocomplete.CompletionProvider; + +import jadx.gui.utils.shortcut.Shortcut; + +public class JadxAutoCompletion extends AutoCompletion + implements IShortcutAction { + public static final String COMMAND = "JadxAutoCompletion.Command"; + + /** + * Constructor. + * + * @param provider The completion provider. This cannot be null + */ + public JadxAutoCompletion(CompletionProvider provider) { + super(provider); + } + + @Override + public ActionModel getActionModel() { + return ActionModel.SCRIPT_AUTO_COMPLETE; + } + + @Override + public JComponent getShortcutComponent() { + return getTextComponent(); + } + + @Override + public void performAction() { + createAutoCompleteAction().actionPerformed( + new ActionEvent(this, ActionEvent.ACTION_PERFORMED, COMMAND)); + } + + @Override + public void setShortcut(Shortcut shortcut) { + if (shortcut != null && shortcut.isKeyboard()) { + setTriggerKey(shortcut.toKeyStroke()); + } else { + setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_UNDEFINED, 0)); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/JadxGuiAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/JadxGuiAction.java new file mode 100644 index 000000000..ab1d1bb7d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/JadxGuiAction.java @@ -0,0 +1,141 @@ +package jadx.gui.ui.action; + +import java.awt.event.ActionEvent; +import java.util.function.Consumer; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.Shortcut; +import jadx.gui.utils.ui.ActionHandler; + +public class JadxGuiAction extends ActionHandler + implements IShortcutAction { + private static final String COMMAND = "JadxGuiAction.Command.%s"; + + private final ActionModel actionModel; + private final String id; + private JComponent shortcutComponent = null; + private KeyStroke addedKeyStroke = null; + private Shortcut shortcut; + + public JadxGuiAction(ActionModel actionModel) { + super(); + this.actionModel = actionModel; + this.id = actionModel.name(); + + updateProperties(); + } + + public JadxGuiAction(ActionModel actionModel, Runnable action) { + super(action); + this.actionModel = actionModel; + this.id = actionModel.name(); + + updateProperties(); + } + + public JadxGuiAction(ActionModel actionModel, Consumer consumer) { + super(consumer); + this.actionModel = actionModel; + this.id = actionModel.name(); + + updateProperties(); + } + + public JadxGuiAction(String id) { + super(); + this.actionModel = null; + this.id = id; + + updateProperties(); + } + + private void updateProperties() { + if (actionModel == null) { + return; + } + + String name = actionModel.getName(); + String description = actionModel.getDescription(); + ImageIcon icon = actionModel.getIcon(); + if (name != null) { + setName(name); + } + if (description != null) { + setShortDescription(description); + } + if (icon != null) { + setIcon(icon); + } + } + + @Nullable + public ActionModel getActionModel() { + return actionModel; + } + + @Override + public void setShortcut(Shortcut shortcut) { + this.shortcut = shortcut; + if (shortcut != null) { + setKeyBinding(shortcut.toKeyStroke()); + } else { + setKeyBinding(null); + } + } + + public void setShortcutComponent(JComponent component) { + this.shortcutComponent = component; + } + + @Override + public JComponent getShortcutComponent() { + return shortcutComponent; + } + + @Override + public void actionPerformed(ActionEvent e) { + super.actionPerformed(e); + } + + @Override + public void performAction() { + if (shortcutComponent != null && !shortcutComponent.isShowing()) { + return; + } + + String shortcutType = "null"; + if (shortcut != null) { + shortcutType = shortcut.getTypeString(); + } + actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, + String.format(COMMAND, shortcutType))); + } + + public static boolean isSource(ActionEvent event) { + return event.getActionCommand() != null + && event.getActionCommand().startsWith(String.format(COMMAND, "")); + } + + @Override + public void setKeyBinding(KeyStroke keyStroke) { + if (shortcutComponent == null) { + super.setKeyBinding(keyStroke); + } else { + // We just set the keyStroke for it to appear in the menu item + // (grayed out in the right) + super.setKeyBinding(keyStroke); + + if (addedKeyStroke != null) { + UiUtils.removeKeyBinding(shortcutComponent, addedKeyStroke, id); + } + UiUtils.addKeyBinding(shortcutComponent, keyStroke, id, this::performAction); + addedKeyStroke = keyStroke; + } + } +} 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 93abf70d3..86bfb24cc 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 @@ -31,6 +31,7 @@ import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.ShortcutsController; /** * The {@link AbstractCodeArea} implementation used for displaying Java code and text based @@ -42,9 +43,12 @@ public final class CodeArea extends AbstractCodeArea { private static final long serialVersionUID = 6312736869579635796L; private @Nullable ICodeInfo cachedCodeInfo; + private final ShortcutsController shortcutsController; CodeArea(ContentPanel contentPanel, JNode node) { super(contentPanel, node); + this.shortcutsController = getMainWindow().getShortcutsController(); + setSyntaxEditingStyle(node.getSyntaxName()); boolean isJavaCode = node instanceof JClass; if (isJavaCode) { @@ -115,7 +119,8 @@ public final class CodeArea extends AbstractCodeArea { } private void addMenuItems() { - JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu()); + ShortcutsController shortcutsController = getMainWindow().getShortcutsController(); + JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu(), shortcutsController); popup.addSeparator(); popup.add(new FindUsageAction(this)); popup.add(new GoToDeclarationAction(this)); @@ -143,7 +148,8 @@ public final class CodeArea extends AbstractCodeArea { } private void addMenuForJsonFile() { - JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu()); + ShortcutsController shortcutsController = getMainWindow().getShortcutsController(); + JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu(), shortcutsController); popup.addSeparator(); popup.add(new JsonPrettifyAction(this)); } @@ -339,6 +345,8 @@ public final class CodeArea extends AbstractCodeArea { @Override public void dispose() { + shortcutsController.unbindActionsForComponent(this); + super.dispose(); cachedCodeInfo = null; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java new file mode 100644 index 000000000..5bda98027 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java @@ -0,0 +1,24 @@ +package jadx.gui.ui.codearea; + +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; + +public class CodeAreaAction extends JadxGuiAction { + protected transient CodeArea codeArea; + + public CodeAreaAction(ActionModel actionModel, CodeArea codeArea) { + super(actionModel); + this.codeArea = codeArea; + setShortcutComponent(codeArea); + } + + public CodeAreaAction(String id, CodeArea codeArea) { + super(id); + this.codeArea = codeArea; + setShortcutComponent(codeArea); + } + + public void dispose() { + codeArea = null; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java index 1799ab77b..cc5db54df 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java @@ -1,9 +1,7 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import javax.swing.AbstractAction; import javax.swing.event.PopupMenuEvent; import org.jetbrains.annotations.Nullable; @@ -25,30 +23,24 @@ import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.dialog.CommentDialog; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import static javax.swing.KeyStroke.getKeyStroke; - -public class CommentAction extends AbstractAction implements DefaultPopupMenuListener { +public class CommentAction extends CodeAreaAction implements DefaultPopupMenuListener { private static final long serialVersionUID = 4753838562204629112L; private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class); - private final CodeArea codeArea; private final boolean enabled; private ICodeComment actionComment; public CommentAction(CodeArea codeArea) { - super(NLS.str("popup.add_comment") + " (;)"); - this.codeArea = codeArea; + super(ActionModel.CODE_COMMENT, codeArea); this.enabled = codeArea.getNode() instanceof JClass; - if (enabled) { - UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment", - () -> showCommentDialog(getCommentRef(codeArea.getCaretPosition()))); - } } @Override @@ -64,7 +56,15 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis @Override public void actionPerformed(ActionEvent e) { - showCommentDialog(this.actionComment); + if (!enabled) { + return; + } + + if (JadxGuiAction.isSource(e)) { + showCommentDialog(getCommentRef(codeArea.getCaretPosition())); + } else { + showCommentDialog(this.actionComment); + } } private void showCommentDialog(ICodeComment codeComment) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java index 347627bf4..5df1a39f3 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java @@ -1,28 +1,15 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; - -import javax.swing.AbstractAction; -import javax.swing.KeyStroke; +import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.dialog.SearchDialog; -import jadx.gui.utils.NLS; -import jadx.gui.utils.UiUtils; -import static javax.swing.KeyStroke.getKeyStroke; - -public class CommentSearchAction extends AbstractAction { +public class CommentSearchAction extends CodeAreaAction { private static final long serialVersionUID = -3646341661734961590L; - private final CodeArea codeArea; - public CommentSearchAction(CodeArea codeArea) { - super(NLS.str("popup.search_comment") + " (Ctrl + ;)"); - this.codeArea = codeArea; - - KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton()); - UiUtils.addKeyBinding(codeArea, key, "popup.search_comment", this::startSearch); + super(ActionModel.CODE_COMMENT_SEARCH, codeArea); } @Override 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 acc7dba55..37b06c945 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,20 +1,15 @@ package jadx.gui.ui.codearea; -import java.awt.event.KeyEvent; - import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; +import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.dialog.UsageDialog; -import jadx.gui.utils.NLS; - -import static javax.swing.KeyStroke.getKeyStroke; public final class FindUsageAction extends JNodeAction { private static final long serialVersionUID = 4692546569977976384L; public FindUsageAction(CodeArea codeArea) { - super(NLS.str("popup.find_usage") + " (x)", codeArea); - addKeyBinding(getKeyStroke(KeyEvent.VK_X, 0), "trigger usage"); + super(ActionModel.FIND_USAGE, codeArea); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java index f932c1eb4..05f4b7f30 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java @@ -1,6 +1,5 @@ package jadx.gui.ui.codearea; -import java.awt.event.KeyEvent; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -23,18 +22,16 @@ import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import static javax.swing.KeyStroke.getKeyStroke; - public final class FridaAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(FridaAction.class); private static final long serialVersionUID = -3084073927621269039L; public FridaAction(CodeArea codeArea) { - super(NLS.str("popup.frida") + " (f)", codeArea); - addKeyBinding(getKeyStroke(KeyEvent.VK_F, 0), "trigger frida"); + super(ActionModel.FRIDA_COPY, codeArea); } @Override 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 89fe11966..3fc92b743 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,18 +1,13 @@ package jadx.gui.ui.codearea; -import java.awt.event.KeyEvent; - import jadx.gui.treemodel.JNode; -import jadx.gui.utils.NLS; - -import static javax.swing.KeyStroke.getKeyStroke; +import jadx.gui.ui.action.ActionModel; public final class GoToDeclarationAction extends JNodeAction { private static final long serialVersionUID = -1186470538894941301L; public GoToDeclarationAction(CodeArea codeArea) { - super(NLS.str("popup.go_to_declaration") + " (d)", codeArea); - addKeyBinding(getKeyStroke(KeyEvent.VK_D, 0), "trigger goto decl"); + super(ActionModel.GOTO_DECLARATION, codeArea); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java index 1d651a9c7..11b84b5ed 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java @@ -3,26 +3,26 @@ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; -import javax.swing.AbstractAction; -import javax.swing.KeyStroke; - import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; -import jadx.gui.utils.UiUtils; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; /** * Add menu and key binding actions for JNode in code area */ -public abstract class JNodeAction extends AbstractAction { +public abstract class JNodeAction extends CodeAreaAction { private static final long serialVersionUID = -2600154727884853550L; - private transient CodeArea codeArea; private transient @Nullable JNode node; - public JNodeAction(String name, CodeArea codeArea) { - super(name); - this.codeArea = codeArea; + public JNodeAction(ActionModel actionModel, CodeArea codeArea) { + super(actionModel, codeArea); + } + + public JNodeAction(String id, CodeArea codeArea) { + super(id, codeArea); } public abstract void runAction(JNode node); @@ -31,21 +31,16 @@ public abstract class JNodeAction extends AbstractAction { return node != null; } - public void addKeyBinding(KeyStroke key, String id) { - UiUtils.addKeyBinding(codeArea, key, id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - node = codeArea.getNodeUnderCaret(); - if (isActionEnabled(node)) { - runAction(node); - } - } - }); - } - @Override public void actionPerformed(ActionEvent e) { - runAction(node); + if (JadxGuiAction.isSource(e)) { + node = codeArea.getNodeUnderCaret(); + if (isActionEnabled(node)) { + runAction(node); + } + } else { + runAction(node); + } } public void changeNode(@Nullable JNode node) { @@ -57,9 +52,10 @@ public abstract class JNodeAction extends AbstractAction { return codeArea; } + @Override public void dispose() { + super.dispose(); node = null; - codeArea = null; for (PropertyChangeListener changeListener : getPropertyChangeListeners()) { removePropertyChangeListener(changeListener); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java index 7ec572b9e..8a0f1f748 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java @@ -1,14 +1,18 @@ package jadx.gui.ui.codearea; -import javax.swing.Action; import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuListener; +import jadx.gui.ui.action.JadxGuiAction; +import jadx.gui.utils.shortcut.ShortcutsController; + public class JNodePopupBuilder { private final JPopupMenu menu; private final JNodePopupListener popupListener; + private final ShortcutsController shortcutsController; - public JNodePopupBuilder(CodeArea codeArea, JPopupMenu popupMenu) { + public JNodePopupBuilder(CodeArea codeArea, JPopupMenu popupMenu, ShortcutsController shortcutsController) { + this.shortcutsController = shortcutsController; menu = popupMenu; popupListener = new JNodePopupListener(codeArea); popupMenu.addPopupMenuListener(popupListener); @@ -19,11 +23,23 @@ public class JNodePopupBuilder { } public void add(JNodeAction nodeAction) { + // We set the shortcut immediately for two reasons + // - there might be multiple instances of this action with + // same ActionModel across different codeAreas, while + // ShortcutController only supports one instance + // - This action will be recreated when shortcuts are changed, + // so no need to bind it + if (nodeAction.getActionModel() != null) { + shortcutsController.bindImmediate(nodeAction); + } menu.add(nodeAction); popupListener.addActions(nodeAction); } - public void add(Action action) { + public void add(JadxGuiAction action) { + if (action.getActionModel() != null) { + shortcutsController.bindImmediate(action); + } menu.add(action); if (action instanceof PopupMenuListener) { menu.addPopupMenuListener((PopupMenuListener) action); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java index 7039deda6..c88e0e71b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java @@ -6,7 +6,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParser; import jadx.gui.treemodel.JNode; -import jadx.gui.utils.NLS; +import jadx.gui.ui.action.ActionModel; public class JsonPrettifyAction extends JNodeAction { @@ -14,7 +14,7 @@ public class JsonPrettifyAction extends JNodeAction { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public JsonPrettifyAction(CodeArea codeArea) { - super(NLS.str("popup.json_prettify"), codeArea); + super(ActionModel.JSON_PRETTIFY, codeArea); } @Override 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 1026e7e1b..41a0d75e3 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,18 +2,14 @@ package jadx.gui.ui.codearea; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRenameNode; +import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.dialog.RenameDialog; -import jadx.gui.utils.NLS; - -import static java.awt.event.KeyEvent.VK_N; -import static javax.swing.KeyStroke.getKeyStroke; public final class RenameAction extends JNodeAction { private static final long serialVersionUID = -4680872086148463289L; public RenameAction(CodeArea codeArea) { - super(NLS.str("popup.rename") + " (n)", codeArea); - addKeyBinding(getKeyStroke(VK_N, 0), "trigger rename"); + super(ActionModel.CODE_RENAME, codeArea); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.java index 6a9e304d1..54c4801d0 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.java @@ -1,6 +1,5 @@ package jadx.gui.ui.codearea; -import java.awt.event.KeyEvent; import java.util.List; import java.util.stream.Collectors; @@ -17,18 +16,16 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import static javax.swing.KeyStroke.getKeyStroke; - public class XposedAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(XposedAction.class); private static final long serialVersionUID = 2641585141624592578L; public XposedAction(CodeArea codeArea) { - super(NLS.str("popup.xposed") + " (y)", codeArea); - addKeyBinding(getKeyStroke(KeyEvent.VK_Y, 0), "trigger xposed"); + super(ActionModel.XPOSED_COPY, codeArea); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/menu/HiddenMenuItem.java b/jadx-gui/src/main/java/jadx/gui/ui/menu/HiddenMenuItem.java new file mode 100644 index 000000000..97812176e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/menu/HiddenMenuItem.java @@ -0,0 +1,22 @@ +package jadx.gui.ui.menu; + +import java.awt.Dimension; +import java.awt.Graphics; + +import javax.swing.Action; +import javax.swing.JMenuItem; + +public class HiddenMenuItem extends JMenuItem { + public HiddenMenuItem(Action a) { + super(a); + } + + @Override + protected void paintComponent(Graphics g) { + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(0, 0); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenu.java b/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenu.java new file mode 100644 index 000000000..17c495070 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenu.java @@ -0,0 +1,64 @@ +package jadx.gui.ui.menu; + +import javax.swing.Action; +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.JadxGuiAction; +import jadx.gui.utils.shortcut.Shortcut; +import jadx.gui.utils.shortcut.ShortcutsController; + +public class JadxMenu extends JMenu { + private final ShortcutsController shortcutsController; + + public JadxMenu(String name, ShortcutsController shortcutsController) { + super(name); + + this.shortcutsController = shortcutsController; + } + + @Override + public JMenuItem add(JMenuItem menuItem) { + Action action = menuItem.getAction(); + bindAction(action); + return super.add(menuItem); + } + + @Override + public JMenuItem add(Action action) { + bindAction(action); + return super.add(action); + } + + public void bindAction(Action action) { + if (action instanceof JadxGuiAction) { + shortcutsController.bind((JadxGuiAction) action); + } + } + + public void reloadShortcuts() { + for (int i = 0; i < getItemCount(); i++) { + // TODO only repaint the items whose shortcut changed + JMenuItem item = getItem(i); + if (item == null) { + continue; + } + + Action action = item.getAction(); + if (!(action instanceof JadxGuiAction) || ((JadxGuiAction) action).getActionModel() == null) { + continue; + } + + ActionModel actionModel = ((JadxGuiAction) action).getActionModel(); + Shortcut shortcut = shortcutsController.get(actionModel); + if (shortcut != null) { + item.setAccelerator(shortcut.toKeyStroke()); + } else { + item.setAccelerator(null); + } + item.repaint(); + item.revalidate(); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenuBar.java b/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenuBar.java new file mode 100644 index 000000000..ed18a937a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenuBar.java @@ -0,0 +1,15 @@ +package jadx.gui.ui.menu; + +import javax.swing.JMenu; +import javax.swing.JMenuBar; + +public class JadxMenuBar extends JMenuBar { + public void reloadShortcuts() { + for (int i = 0; i < getMenuCount(); i++) { + JMenu menu = getMenu(i); + if (menu instanceof JadxMenu) { + ((JadxMenu) menu).reloadShortcuts(); + } + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/shortcut/Shortcut.java b/jadx-gui/src/main/java/jadx/gui/utils/shortcut/Shortcut.java new file mode 100644 index 000000000..10ed55d6c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/shortcut/Shortcut.java @@ -0,0 +1,150 @@ +package jadx.gui.utils.shortcut; + +import java.awt.event.KeyEvent; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.swing.KeyStroke; + +import jadx.gui.utils.SystemInfo; + +public class Shortcut { + private static final Set FORBIDDEN_KEY_CODES = new HashSet<>(List.of( + KeyEvent.VK_UNDEFINED, KeyEvent.VK_SHIFT, KeyEvent.VK_ALT, KeyEvent.VK_META, KeyEvent.VK_ALT_GRAPH)); + private static final Set ALLOWED_MODIFIERS = new HashSet<>(List.of( + KeyEvent.CTRL_DOWN_MASK, KeyEvent.META_DOWN_MASK, KeyEvent.ALT_DOWN_MASK, KeyEvent.ALT_GRAPH_DOWN_MASK, + KeyEvent.SHIFT_DOWN_MASK)); + + private Integer keyCode = null; + private Integer modifiers = null; + private Integer mouseButton = null; + + private Shortcut() { + } + + public static Shortcut keyboard(int keyCode) { + return keyboard(keyCode, 0); + } + + public static Shortcut keyboard(int keyCode, int modifiers) { + Shortcut shortcut = new Shortcut(); + shortcut.keyCode = keyCode; + shortcut.modifiers = modifiers; + return shortcut; + } + + public static Shortcut mouse(int mouseButton) { + Shortcut shortcut = new Shortcut(); + shortcut.mouseButton = mouseButton; + return shortcut; + } + + public static Shortcut none() { + Shortcut shortcut = new Shortcut(); + // Must have at least one non-null attribute in order to be serialized + // otherwise will roll back to default Shortcut + shortcut.modifiers = 0; + return shortcut; + } + + public Integer getKeyCode() { + return keyCode; + } + + public Integer getModifiers() { + return modifiers; + } + + public Integer getMouseButton() { + return mouseButton; + } + + public boolean isKeyboard() { + return keyCode != null; + } + + public boolean isMouse() { + return mouseButton != null; + } + + public boolean isNone() { + return !isMouse() && !isKeyboard(); + } + + public boolean isValidKeyboard() { + return isKeyboard() && !FORBIDDEN_KEY_CODES.contains(keyCode) && isValidModifiers(); + } + + public boolean isValidModifiers() { + int modifiersTest = modifiers; + for (Integer modifier : ALLOWED_MODIFIERS) { + modifiersTest &= ~modifier; + } + return modifiersTest == 0; + } + + public KeyStroke toKeyStroke() { + return isKeyboard() + ? KeyStroke.getKeyStroke(keyCode, modifiers, + modifiers != 0 && SystemInfo.IS_MAC) + : null; + } + + @Override + public String toString() { + if (isKeyboard()) { + return keyToString(); + } else if (isMouse()) { + return mouseToString(); + } + return "NONE"; + } + + public String getTypeString() { + if (isKeyboard()) { + return "Keyboard"; + } else if (isMouse()) { + return "Mouse"; + } + return null; + } + + private String mouseToString() { + return "MouseButton" + mouseButton; + } + + private String keyToString() { + StringBuilder sb = new StringBuilder(); + if (modifiers != null && modifiers > 0) { + sb.append(KeyEvent.getModifiersExText(modifiers)); + sb.append('+'); + } + if (keyCode != null && keyCode != 0) { + sb.append(KeyEvent.getKeyText(keyCode)); + } else { + sb.append("UNDEFINED"); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Shortcut shortcut = (Shortcut) o; + return Objects.equals(keyCode, shortcut.keyCode) + && Objects.equals(modifiers, shortcut.modifiers) + && Objects.equals(mouseButton, shortcut.mouseButton); + } + + @Override + public int hashCode() { + return Objects.hash(keyCode, modifiers, mouseButton); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/shortcut/ShortcutsController.java b/jadx-gui/src/main/java/jadx/gui/utils/shortcut/ShortcutsController.java new file mode 100644 index 000000000..b9f0e9bc1 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/shortcut/ShortcutsController.java @@ -0,0 +1,143 @@ +package jadx.gui.utils.shortcut; + +import java.awt.AWTEvent; +import java.awt.Toolkit; +import java.awt.event.MouseEvent; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.swing.JComponent; +import javax.swing.KeyStroke; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.settings.data.ShortcutsWrapper; +import jadx.gui.ui.MainWindow; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.IShortcutAction; +import jadx.gui.utils.UiUtils; + +public class ShortcutsController { + private ShortcutsWrapper shortcuts; + private final JadxSettings settings; + + private final Map> boundActions = new HashMap<>(); + + private Set mouseActions = null; + + public ShortcutsController(JadxSettings settings) { + this.settings = settings; + } + + public void loadSettings() { + this.shortcuts = settings.getShortcuts(); + + indexMouseActions(); + + for (Map.Entry> actionsEntry : boundActions.entrySet()) { + ActionModel actionModel = actionsEntry.getKey(); + Set actions = actionsEntry.getValue(); + Shortcut shortcut = get(actionModel); + if (actions != null) { + for (IShortcutAction action : actions) { + action.setShortcut(shortcut); + } + } + } + } + + @Nullable + public Shortcut get(ActionModel actionModel) { + return shortcuts.get(actionModel); + } + + public KeyStroke getKeyStroke(ActionModel actionModel) { + Shortcut shortcut = get(actionModel); + KeyStroke keyStroke = null; + if (shortcut != null && shortcut.isKeyboard()) { + keyStroke = shortcut.toKeyStroke(); + } + return keyStroke; + } + + /* + * Binds to an action and updates its shortcut every time loadSettings is called + */ + public void bind(IShortcutAction action) { + boundActions.computeIfAbsent(action.getActionModel(), k -> new HashSet<>()); + boundActions.get(action.getActionModel()).add(action); + } + + /* + * Immediately sets the shortcut for an action + */ + public void bindImmediate(IShortcutAction action) { + bind(action); + Shortcut shortcut = get(action.getActionModel()); + action.setShortcut(shortcut); + } + + public static Map getDefault() { + Map shortcuts = new HashMap<>(); + for (ActionModel actionModel : ActionModel.values()) { + shortcuts.put(actionModel, actionModel.getDefaultShortcut()); + } + return shortcuts; + } + + public void registerMouseEventListener(MainWindow mw) { + Toolkit.getDefaultToolkit().addAWTEventListener(event -> { + if (mw.isSettingsOpen()) { + return; + } + + if (!(event instanceof MouseEvent)) { + return; + } + MouseEvent mouseEvent = (MouseEvent) event; + if (mouseEvent.getID() != MouseEvent.MOUSE_PRESSED) { + return; + } + + int mouseButton = mouseEvent.getButton(); + for (ActionModel actionModel : mouseActions) { + Shortcut shortcut = shortcuts.get(actionModel); + if (shortcut != null && shortcut.getMouseButton() == mouseButton) { + Set actions = boundActions.get(actionModel); + if (actions != null) { + for (IShortcutAction action : actions) { + if (action != null) { + mouseEvent.consume(); + UiUtils.uiRun(action::performAction); + } + } + } + } + } + }, AWTEvent.MOUSE_EVENT_MASK); + } + + private void indexMouseActions() { + mouseActions = new HashSet<>(); + for (ActionModel actionModel : ActionModel.values()) { + Shortcut shortcut = shortcuts.get(actionModel); + if (shortcut != null && shortcut.isMouse()) { + mouseActions.add(actionModel); + } else { + mouseActions.remove(actionModel); + } + } + } + + public void unbindActionsForComponent(JComponent component) { + for (ActionModel actionModel : ActionModel.values()) { + Set actions = boundActions.get(actionModel); + if (actions != null) { + actions.removeIf(action -> action != null && action.getShortcutComponent() == component); + } + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java index 67b354599..763c61da6 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java @@ -1,7 +1,6 @@ package jadx.gui.utils.ui; import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; import java.util.function.Consumer; import javax.swing.AbstractAction; @@ -11,6 +10,7 @@ import javax.swing.JComponent; import javax.swing.KeyStroke; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.shortcut.Shortcut; public class ActionHandler extends AbstractAction { @@ -24,6 +24,11 @@ public class ActionHandler extends AbstractAction { this.consumer = consumer; } + public ActionHandler() { + this.consumer = ev -> { + }; + } + public void setName(String name) { putValue(NAME, name); } @@ -58,7 +63,7 @@ public class ActionHandler extends AbstractAction { public void addKeyBindToDescription() { KeyStroke keyStroke = (KeyStroke) getValue(ACCELERATOR_KEY); if (keyStroke != null) { - String keyText = KeyEvent.getKeyText(keyStroke.getKeyCode()); + String keyText = Shortcut.keyboard(keyStroke.getKeyCode(), keyStroke.getModifiers()).toString(); String desc = (String) getValue(SHORT_DESCRIPTION); setShortDescription(desc + " (" + keyText + ")"); } 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 8c226f49c..76fa6ea06 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -23,6 +23,7 @@ menu.deobfuscation=Deobfuskierung menu.log=Log-Anzeige menu.help=Hilfe menu.about=Über +#menu.quark= menu.update_label=Neue Version %s verfügbar! file.open_action=Datei öffnen… @@ -161,6 +162,8 @@ about_dialog.title=Über JADX preferences.title=Einstellungen preferences.deobfuscation=Deobfuskierung preferences.appearance=Aussehen +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=Dekompilierung preferences.plugins=Plugins preferences.project=Projekt @@ -257,6 +260,9 @@ msg.open_file=Bitte Datei öffnen msg.saving_sources=Quelltexte speichern msg.language_changed_title=Sprache speichern msg.language_changed=Die neue Sprache wird beim nächsten Start der Anwendung angezeigt. +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=Fehler msg.project_error=Projekt konnte nicht geladen werden msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht. @@ -290,6 +296,7 @@ popup.search_global=Globale Suche "%s" #script.run=Run #script.save=Save +#script.auto_complete= #script.check=Check #script.format=Reformat #script.log=Show log @@ -405,3 +412,8 @@ adb_dialog.no_devices=Ich kann kein Gerät finden, um die App zu starten. adb_dialog.restart_while_debugging_title=Neustart während der Fehlersuche adb_dialog.restart_while_debugging_msg=Sie debuggen eine App, sind Sie sicher, dass Sie eine Sitzung neu starten können? adb_dialog.starting_debugger=Debugger starten… + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= 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 67e2ded09..4b0fec165 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -23,6 +23,7 @@ menu.deobfuscation=Deobfuscation menu.log=Log Viewer menu.help=Help menu.about=About +menu.quark=Quark Engine menu.update_label=New version %s available! file.open_action=Open files ... @@ -161,6 +162,8 @@ about_dialog.title=About JADX preferences.title=Preferences preferences.deobfuscation=Deobfuscation preferences.appearance=Appearance +preferences.shortcuts=Shortcuts +preferences.select_shortcuts=Select a specific shortcuts group preferences.decompile=Decompilation preferences.plugins=Plugins preferences.project=Project @@ -257,6 +260,9 @@ msg.open_file=Please open file msg.saving_sources=Saving sources msg.language_changed_title=Language changed msg.language_changed=New language will be displayed the next time application starts. +msg.warning_title=Warning +msg.common_mouse_shortcut=This is a commonly used key, are you sure you would like to bind it to an action? +msg.duplicate_shortcut=The shortcut %s is already set in action "%s" from category "%s", continue ? msg.project_error_title=Error msg.project_error=Project could not be loaded msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. @@ -290,6 +296,7 @@ popup.json_prettify=JSON Prettify script.run=Run script.save=Save +script.auto_complete=Auto Complete script.check=Check script.format=Reformat script.log=Show log @@ -405,3 +412,8 @@ adb_dialog.no_devices=Can't found any device to start app. adb_dialog.restart_while_debugging_title=Restart while debugging adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session? adb_dialog.starting_debugger=Starting debugger... + +action.variant=%s (variant) +action_category.menu_toolbar=Menu / Toolbar +action_category.code_area=Code Area +action_category.plugin_script=Plugin Script 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 ecf4252aa..6d2feeb68 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -23,6 +23,7 @@ menu.deobfuscation=Desofuscación menu.log=Visor log menu.help=Ayuda menu.about=Acerca de... +#menu.quark= menu.update_label=¡Nueva versión %s disponible! file.open_action=Abrir archivo... @@ -161,6 +162,8 @@ about_dialog.title=Sobre JADX preferences.title=Preferencias preferences.deobfuscation=Desofuscación #preferences.appearance=Appearance +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=Descompilación #preferences.plugins=Plugins #preferences.project= @@ -257,6 +260,9 @@ msg.open_file=Por favor, abra un archivo msg.saving_sources=Guardando fuente msg.language_changed_title=Idioma cambiado msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie. +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= #msg.project_error_title= #msg.project_error= #msg.cmd_select_class_error= @@ -290,6 +296,7 @@ popup.rename=Nimeta ümber #script.run=Run #script.save=Save +#script.auto_complete= #script.check=Check #script.format=Reformat #script.log=Show log @@ -405,3 +412,8 @@ certificate.serialPubKeyY=Y #adb_dialog.restart_while_debugging_title=Restart while debugging #adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session? #adb_dialog.starting_debugger=Starting debugger... + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= 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 66b0ccfbf..101209d0f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -23,6 +23,7 @@ menu.deobfuscation=난독화 해제 menu.log=로그 뷰어 menu.help=도움말 menu.about=정보 +#menu.quark= menu.update_label=새 버전 %s 이(가) 존재합니다! file.open_action=파일 열기 ... @@ -161,6 +162,8 @@ about_dialog.title=JADX 정보 preferences.title=설정 preferences.deobfuscation=난독화 해제 preferences.appearance=외관 +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=디컴파일 preferences.plugins=플러그인 preferences.project=프로젝트 @@ -257,6 +260,9 @@ msg.open_file=파일을 여십시오 msg.saving_sources=소스 저장 중 msg.language_changed_title=언어 변경됨 msg.language_changed=다음에 응용 프로그램이 시작되면 새 언어가 표시됩니다. +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=오류 msg.project_error=프로젝트를 로드 할 수 없습니다. msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다. @@ -290,6 +296,7 @@ popup.search_global="%s" 전역 검색 #script.run=Run #script.save=Save +#script.auto_complete= #script.check=Check #script.format=Reformat #script.log=Show log @@ -405,3 +412,8 @@ adb_dialog.no_devices=앱을 실행할 기기를 찾을 수 없습니다. adb_dialog.restart_while_debugging_title=디버깅중 다시 시작 adb_dialog.restart_while_debugging_msg=앱을 디버깅하고 있습니다. 세션을 다시 시작 하시겠습니까? adb_dialog.starting_debugger=디버거 시작 중 ... + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 218b9e6b3..87ee64037 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -23,6 +23,7 @@ menu.deobfuscation=Desofuscar menu.log=Visualizador de log menu.help=Ajuda menu.about=Sobre +#menu.quark= menu.update_label=Nova versão %s disponível! file.open_action=Abrir arquivos... @@ -161,6 +162,8 @@ about_dialog.title=Sobre o JADX preferences.title=Preferências preferences.deobfuscation=Desofuscar preferences.appearance=Aparência +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=Descompilação preferences.plugins=Plugins preferences.project=Projeto @@ -257,6 +260,9 @@ msg.open_file=Abra um arquivo msg.saving_sources=Salvando recursos msg.language_changed_title=Idioma alterado msg.language_changed=Novo idioma será mostrado na próxima inicialização. +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=Erro msg.project_error=Projeto não pôde ser carregado msg.cmd_select_class_error=Falha ao selecionar classe\n%s\nA classe não existe. @@ -290,6 +296,7 @@ popup.search_global=Busca global "%s" #script.run=Run #script.save=Save +#script.auto_complete= #script.check=Check #script.format=Reformat #script.log=Show log @@ -405,3 +412,8 @@ adb_dialog.no_devices=Não foi possível encontrar nenhum dispositivo para inici adb_dialog.restart_while_debugging_title=Reiniciar enquanto depura adb_dialog.restart_while_debugging_msg=Você está depurando um aplicativo, você tem certeza que deseja reiniciar a sessão? adb_dialog.starting_debugger=Iniciando depurador... + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index f1c7d9475..0d65a875b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -23,6 +23,7 @@ menu.deobfuscation=Деобфускация menu.log=Просмотр логов menu.help=Помощь menu.about=О программе +#menu.quark= menu.update_label=Версия %s уже доступна! file.open_action=Открыть файлы... @@ -161,6 +162,8 @@ about_dialog.title=О программе JADX preferences.title=Параметры preferences.deobfuscation=Деобфускация preferences.appearance=Внешний вид +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=Декомпиляция preferences.plugins=Плагины preferences.project=Проект @@ -257,6 +260,9 @@ msg.open_file=Пожалуйста, откройте файл msg.saving_sources=Сохранение ресурсов msg.language_changed_title=Язык изменен msg.language_changed=Новый язык применится при следующем запуске программы +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=Ошибка msg.project_error=Проект не может быть загружен msg.cmd_select_class_error=Ошибка выбора класса\n%s\nЭтот класс не существует. @@ -290,6 +296,7 @@ popup.search_global=Глобальный поиск "%s" #script.run=Run #script.save=Save +#script.auto_complete= #script.check=Check #script.format=Reformat #script.log=Show log @@ -405,3 +412,8 @@ adb_dialog.no_devices=Нет устройств для запуска прило adb_dialog.restart_while_debugging_title=Перезапустить отладчик adb_dialog.restart_while_debugging_msg=Запущен процесс отдадки приложения. Вы действительно хотите перезапустить сессию? adb_dialog.starting_debugger=Запуск отладки... + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= 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 00d78451b..ccbf1c6ca 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -23,6 +23,7 @@ menu.deobfuscation=反混淆 menu.log=日志查看器 menu.help=帮助 menu.about=关于 +#menu.quark= menu.update_label=发现新版本 %s! file.open_action=打开文件… @@ -161,6 +162,8 @@ about_dialog.title=关于 JADX preferences.title=首选项 preferences.deobfuscation=反混淆 preferences.appearance=界面 +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=反编译 preferences.plugins=插件 preferences.project=项目 @@ -257,6 +260,9 @@ msg.open_file=请打开文件 msg.saving_sources=正在导出源代码 msg.language_changed_title=语言已更改 msg.language_changed=新的语言将在下次应用程序启动时显示。 +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=错误 msg.project_error=项目无法加载 msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 @@ -290,6 +296,7 @@ popup.json_prettify=JSON 格式化 script.run=运行 script.save=保存 +#script.auto_complete= script.check=检查 script.format=重新格式化 script.log=显示日志 @@ -405,3 +412,8 @@ adb_dialog.no_devices=找不到任何设备用来启动APP。 adb_dialog.restart_while_debugging_title=调试时重新启动 adb_dialog.restart_while_debugging_msg=你正在调试一个APP,确定要重新启动一个会话吗? adb_dialog.starting_debugger=正在启动调试器… + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script= diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 5e88e1cb9..6265e07b2 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -23,6 +23,7 @@ menu.deobfuscation=去模糊化 menu.log=記錄檔檢視器 menu.help=幫助 menu.about=關於 +#menu.quark= menu.update_label=新版本 %s 可供下載! file.open_action=開啟檔案... @@ -161,6 +162,8 @@ about_dialog.title=關於 JADX preferences.title=選項 preferences.deobfuscation=去模糊化 preferences.appearance=外觀 +#preferences.shortcuts= +#preferences.select_shortcuts= preferences.decompile=反編譯 preferences.plugins=外掛程式 preferences.project=專案 @@ -257,6 +260,9 @@ msg.open_file=請開啟檔案 msg.saving_sources=正在儲存原始碼 msg.language_changed_title=已更改語言 msg.language_changed=新語言將於下次應用程式啟動時套用。 +#msg.warning_title= +#msg.common_mouse_shortcut= +#msg.duplicate_shortcut= msg.project_error_title=錯誤 msg.project_error=無法載入專案 msg.cmd_select_class_error=無法選擇類別\n%s\n類別不存在。 @@ -290,6 +296,7 @@ popup.json_prettify=JSON 格式化 script.run=執行 script.save=儲存 +#script.auto_complete= script.check=檢查 script.format=重新格式化 script.log=顯示記錄檔 @@ -405,3 +412,8 @@ adb_dialog.no_devices=無法找到任何可以啟動應用程式的裝置。 adb_dialog.restart_while_debugging_title=偵錯時重新啟動 adb_dialog.restart_while_debugging_msg=您正在為應用程式偵錯,您確定要重新啟動工作階段嗎? adb_dialog.starting_debugger=正在啟動偵錯工具... + +#action.variant= +#action_category.menu_toolbar= +#action_category.code_area= +#action_category.plugin_script=