Skip to content

Commit

Permalink
feat(gui): allow user to set custom shortcuts (#1479)(PR #1980)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Mino260806 and skylot committed Aug 3, 2023
1 parent 868e99e commit 68b84ea
Show file tree
Hide file tree
Showing 40 changed files with 1,433 additions and 399 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ private static class NodeAction extends JNodeAction {

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -27,19 +25,20 @@ public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) {
setCodeFoldingEnabled(true);
setCloseCurlyBraces(true);

shortcutsController = contentPanel.getTabbedPane().getMainWindow().getShortcutsController();
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
autoCompletion = addAutoComplete(settings);
}

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;
}

Expand Down Expand Up @@ -80,6 +79,7 @@ public JInputScript getScriptNode() {

@Override
public void dispose() {
shortcutsController.unbindActionsForComponent(this);
autoCompletion.uninstall();
super.dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@
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;
Expand Down Expand Up @@ -89,15 +90,14 @@ private void initUI() {
}

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));

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()));
JadxGuiAction runAction = new JadxGuiAction(ActionModel.SCRIPT_RUN, this::runScript);
JadxGuiAction saveAction = new JadxGuiAction(ActionModel.SCRIPT_SAVE, scriptArea::save);

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);
Expand Down
15 changes: 15 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@
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);
Expand Down Expand Up @@ -73,6 +76,10 @@ public class JadxSettings extends JadxCLIArgs {
private boolean autoStartJobs = false;
private String excludedPackages = "";
private boolean autoSaveProject = true;
private Map<ActionModel, Shortcut> shortcuts = new HashMap<>();

@JadxSettingsAdapter.GsonExclude
private ShortcutsWrapper shortcutsWrapper = null;

private boolean showHeapUsageBar = false;
private boolean alwaysSelectOpened = false;
Expand Down Expand Up @@ -442,6 +449,14 @@ public void setAutoSaveProject(boolean autoSaveProject) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ActionModel, Shortcut> shortcuts;

public void updateShortcuts(Map<ActionModel, Shortcut> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
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;
Expand Down Expand Up @@ -126,6 +127,7 @@ private void initUI() {
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());
Expand Down Expand Up @@ -640,6 +642,7 @@ private void save() {
enableComponents(this, false);
SwingUtilities.invokeLater(() -> {
if (shouldReload()) {
mainWindow.getShortcutsController().loadSettings();
mainWindow.reopen();
}
if (!settings.getLangLocale().equals(prevLang)) {
Expand Down
193 changes: 193 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading

0 comments on commit 68b84ea

Please sign in to comment.