feat(gui): manage plugins in preferences window

This commit is contained in:
Skylot
2023-06-06 20:25:01 +01:00
parent a72e6aeafe
commit 50c5f0874f
33 changed files with 677 additions and 73 deletions
@@ -1,6 +1,9 @@
package jadx.api.plugins.events;
import jadx.api.plugins.events.types.NodeRenamedByUser;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiSettings;
import static jadx.api.plugins.events.JadxEventType.create;
@@ -13,4 +16,11 @@ public class JadxEvents {
* Notify about renames done by user (GUI only).
*/
public static final JadxEventType<NodeRenamedByUser> NODE_RENAMED_BY_USER = create();
/**
* Request reload of settings window (GUI only).
* Useful for reload custom settings group set with
* {@link JadxGuiSettings#setCustomSettingsGroup(ISettingsGroup)}.
*/
public static final JadxEventType<ReloadSettingsWindow> RELOAD_SETTINGS_WINDOW = create();
}
@@ -0,0 +1,24 @@
package jadx.api.plugins.events.types;
import jadx.api.plugins.events.IJadxEvent;
import jadx.api.plugins.events.JadxEventType;
import jadx.api.plugins.events.JadxEvents;
public class ReloadSettingsWindow implements IJadxEvent {
public static final ReloadSettingsWindow INSTANCE = new ReloadSettingsWindow();
private ReloadSettingsWindow() {
// singleton
}
@Override
public JadxEventType<ReloadSettingsWindow> getType() {
return JadxEvents.RELOAD_SETTINGS_WINDOW;
}
@Override
public String toString() {
return "RELOAD_SETTINGS_WINDOW";
}
}
@@ -9,7 +9,7 @@ public interface JadxGuiSettings {
/**
* Set plugin custom settings page
*/
void setCustomSettings(ISettingsGroup group);
void setCustomSettingsGroup(ISettingsGroup group);
/**
* Helper method to build options group only for provided option list
@@ -7,5 +7,5 @@ public enum TaskStatus {
CANCEL_BY_USER,
CANCEL_BY_TIMEOUT,
CANCEL_BY_MEMORY,
ERROR;
ERROR
}
@@ -5,8 +5,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JMenu;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,9 +39,7 @@ public class CommonGuiPluginsContext {
public void reset() {
codePopupActionList.clear();
JMenu pluginsMenu = mainWindow.getPluginsMenu();
pluginsMenu.removeAll();
pluginsMenu.setVisible(false);
mainWindow.resetPluginsMenu();
}
public MainWindow getMainWindow() {
@@ -63,9 +59,7 @@ public class CommonGuiPluginsContext {
}
});
item.setNameAndDesc(name);
JMenu pluginsMenu = mainWindow.getPluginsMenu();
pluginsMenu.add(item);
pluginsMenu.setVisible(true);
mainWindow.addToPluginsMenu(item);
}
public void appendPopupMenus(CodeArea codeArea, JNodePopupBuilder popup) {
@@ -5,8 +5,8 @@ import java.util.List;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiSettings;
import jadx.api.plugins.options.OptionDescription;
import jadx.gui.settings.ui.PluginsSettings;
import jadx.gui.settings.ui.SubSettingsGroup;
import jadx.gui.settings.ui.plugins.PluginsSettings;
import jadx.gui.ui.MainWindow;
public class GuiSettingsContext implements JadxGuiSettings {
@@ -17,7 +17,7 @@ public class GuiSettingsContext implements JadxGuiSettings {
}
@Override
public void setCustomSettings(ISettingsGroup group) {
public void setCustomSettingsGroup(ISettingsGroup group) {
guiPluginContext.setCustomSettings(group);
}
@@ -37,6 +37,7 @@ import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.tree.TreePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,12 +55,14 @@ import jadx.api.JadxDecompiler;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.plugins.events.JadxEvents;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.usage.UsageCacheMode;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsAdapter;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.settings.ui.plugins.PluginsSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorTheme;
import jadx.gui.utils.FontUtils;
@@ -82,6 +85,7 @@ public class JadxSettingsWindow extends JDialog {
private final transient LangLocale prevLang;
private transient boolean needReload = false;
private SettingsTree tree;
public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) {
this.mainWindow = mainWindow;
@@ -91,6 +95,7 @@ public class JadxSettingsWindow extends JDialog {
this.prevLang = settings.getLangLocale();
initUI();
mainWindow.events().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, r -> UiUtils.uiRun(this::reloadUI));
setTitle(NLS.str("preferences.title"));
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
@@ -103,10 +108,18 @@ public class JadxSettingsWindow extends JDialog {
}
}
private void reloadUI() {
TreePath selectionPath = tree.getSelectionPath();
mainWindow.getSettings().saveWindowPos(this);
getContentPane().removeAll();
initUI();
pack();
mainWindow.getSettings().loadWindowPos(this);
tree.setSelectionPath(selectionPath);
}
private void initUI() {
JPanel groupPanel = new JPanel();
groupPanel.setLayout(new BoxLayout(groupPanel, BoxLayout.LINE_AXIS));
groupPanel.setBorder(BorderFactory.createEmptyBorder(10, 3, 3, 10));
JPanel wrapGroupPanel = new JPanel(new BorderLayout(10, 10));
List<ISettingsGroup> groups = new ArrayList<>();
groups.add(makeDecompilationGroup());
@@ -118,18 +131,15 @@ public class JadxSettingsWindow extends JDialog {
groups.add(new PluginsSettings(mainWindow, settings).build());
groups.add(makeOtherGroup());
SettingsTree tree = new SettingsTree();
tree.init(groupPanel, groups);
tree = new SettingsTree();
tree.init(wrapGroupPanel, groups);
JScrollPane leftPane = new JScrollPane(tree);
leftPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 3, 3));
JPanel wrapGroupPanel = new JPanel(new BorderLayout());
wrapGroupPanel.add(groupPanel, BorderLayout.PAGE_START);
JScrollPane rightPane = new JScrollPane(wrapGroupPanel);
rightPane.getVerticalScrollBar().setUnitIncrement(16);
rightPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
rightPane.setBorder(BorderFactory.createEmptyBorder());
rightPane.setBorder(BorderFactory.createEmptyBorder(10, 3, 3, 10));
JSplitPane splitPane = new JSplitPane();
splitPane.setResizeWeight(0.2);
@@ -1,5 +1,6 @@
package jadx.gui.settings.ui;
import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
@@ -18,16 +19,21 @@ public class SettingsGroup implements ISettingsGroup {
private final String title;
private final JPanel panel;
private final JPanel gridPanel;
private final GridBagConstraints c;
private int row;
public SettingsGroup(String title) {
this.title = title;
panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder(title));
gridPanel = new JPanel(new GridBagLayout());
c = new GridBagConstraints();
c.insets = new Insets(5, 5, 5, 5);
c.weighty = 1.0;
panel = new JPanel();
panel.setLayout(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder(title));
panel.add(gridPanel, BorderLayout.PAGE_START);
}
public JLabel addRow(String label, JComponent comp) {
@@ -36,15 +42,15 @@ public class SettingsGroup implements ISettingsGroup {
public JLabel addRow(String label, String tooltip, JComponent comp) {
c.gridy = row++;
JLabel jLabel = new JLabel(label);
jLabel.setLabelFor(comp);
jLabel.setHorizontalAlignment(SwingConstants.LEFT);
JLabel rowLbl = new JLabel(label);
rowLbl.setLabelFor(comp);
rowLbl.setHorizontalAlignment(SwingConstants.LEFT);
c.gridx = 0;
c.gridwidth = 1;
c.anchor = GridBagConstraints.LINE_START;
c.weightx = 0.8;
c.fill = GridBagConstraints.NONE;
panel.add(jLabel, c);
gridPanel.add(rowLbl, c);
c.gridx = 1;
c.gridwidth = GridBagConstraints.REMAINDER;
c.anchor = GridBagConstraints.CENTER;
@@ -52,18 +58,16 @@ public class SettingsGroup implements ISettingsGroup {
c.fill = GridBagConstraints.HORIZONTAL;
if (tooltip != null) {
jLabel.setToolTipText(tooltip);
rowLbl.setToolTipText(tooltip);
comp.setToolTipText(tooltip);
}
panel.add(comp, c);
comp.addPropertyChangeListener("enabled", evt -> jLabel.setEnabled((boolean) evt.getNewValue()));
return jLabel;
gridPanel.add(comp, c);
comp.addPropertyChangeListener("enabled", evt -> rowLbl.setEnabled((boolean) evt.getNewValue()));
return rowLbl;
}
public void end() {
panel.add(Box.createVerticalGlue());
gridPanel.add(Box.createVerticalGlue());
}
@Override
@@ -0,0 +1,20 @@
package jadx.gui.settings.ui.plugins;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.JadxPluginInfo;
abstract class BasePluginListNode {
public @Nullable String getTitle() {
return null;
}
public JadxPluginInfo getPluginInfo() {
return null;
}
public @Nullable String getVersion() {
return null;
}
}
@@ -0,0 +1,140 @@
package jadx.gui.settings.ui.plugins;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.formdev.flatlaf.FlatClientProperties;
import ch.qos.logback.classic.Level;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.gui.logs.LogOptions;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class InstallPluginDialog extends JDialog {
private static final Logger LOG = LoggerFactory.getLogger(InstallPluginDialog.class);
private static final long serialVersionUID = 5304314264730563853L;
private final MainWindow mainWindow;
private JTextField locationFld;
public InstallPluginDialog(MainWindow mainWindow) {
super(mainWindow, NLS.str("preferences.plugins.install"));
this.mainWindow = mainWindow;
init();
}
private void init() {
locationFld = new JTextField();
locationFld.setAlignmentX(LEFT_ALIGNMENT);
locationFld.setColumns(50);
TextStandardActions.attach(locationFld);
locationFld.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
JLabel locationLbl = new JLabel(NLS.str("preferences.plugins.location_id_label"));
locationLbl.setLabelFor(locationFld);
JPanel locationPanel = new JPanel();
locationPanel.setLayout(new BoxLayout(locationPanel, BoxLayout.LINE_AXIS));
locationPanel.add(locationLbl);
locationPanel.add(Box.createRigidArea(new Dimension(5, 0)));
locationPanel.add(locationFld);
JButton fileBtn = new JButton(NLS.str("preferences.plugins.plugin_jar"));
fileBtn.addActionListener(ev -> openPluginJar());
JLabel fileLbl = new JLabel(NLS.str("preferences.plugins.plugin_jar_label"));
fileLbl.setLabelFor(fileBtn);
JPanel filePanel = new JPanel();
filePanel.setLayout(new BoxLayout(filePanel, BoxLayout.LINE_AXIS));
filePanel.add(fileLbl);
filePanel.add(Box.createRigidArea(new Dimension(5, 0)));
filePanel.add(fileBtn);
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.add(locationPanel);
mainPanel.add(Box.createRigidArea(new Dimension(0, 5)));
mainPanel.add(filePanel);
JButton installBtn = new JButton(NLS.str("preferences.plugins.install_btn"));
installBtn.addActionListener(ev -> install());
JButton cancelBtn = new JButton(NLS.str("preferences.cancel"));
cancelBtn.addActionListener(ev -> dispose());
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
// TODO: add operation progress
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(installBtn);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelBtn);
getRootPane().setDefaultButton(installBtn);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(mainPanel, BorderLayout.PAGE_START);
contentPanel.add(buttonPane, BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
pack();
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.APPLICATION_MODAL);
UiUtils.addEscapeShortCutToDispose(this);
}
private void openPluginJar() {
FileDialogWrapper fd = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_OPEN);
fd.setTitle(NLS.str("preferences.plugins.plugin_jar"));
fd.setFileExtList(Collections.singletonList("jar"));
fd.setSelectionMode(JFileChooser.FILES_ONLY);
List<Path> files = fd.show();
if (files.size() == 1) {
locationFld.setText("file:" + files.get(0).toAbsolutePath());
}
}
private void install() {
mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"),
() -> {
try {
JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationFld.getText());
LOG.info("Plugin installed: {}", metadata);
} catch (Exception e) {
LOG.error("Install failed", e);
mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR));
}
},
status -> {
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
});
dispose();
}
}
@@ -0,0 +1,39 @@
package jadx.gui.settings.ui.plugins;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.JadxPluginInfo;
import jadx.core.plugins.PluginContext;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class PluginListNode extends BasePluginListNode {
private final PluginContext plugin;
private final JadxPluginMetadata metadata;
public PluginListNode(PluginContext plugin, @Nullable JadxPluginMetadata metadata) {
this.plugin = plugin;
this.metadata = metadata;
}
@Override
public JadxPluginInfo getPluginInfo() {
return plugin.getPluginInfo();
}
public @Nullable JadxPluginMetadata getMetadata() {
return metadata;
}
@Override
public @Nullable String getVersion() {
if (metadata != null) {
return metadata.getVersion();
}
return null;
}
@Override
public String toString() {
return plugin.getPluginInfo().getName();
}
}
@@ -1,4 +1,4 @@
package jadx.gui.settings.ui;
package jadx.gui.settings.ui.plugins;
import java.awt.event.ItemEvent;
import java.util.List;
@@ -11,13 +11,13 @@ import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiContext;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -25,13 +25,18 @@ import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.OptionType;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils;
import jadx.gui.plugins.context.GuiPluginContext;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.SettingsGroup;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.plugins.CollectPluginOptions;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.plugins.CollectPlugins;
import jadx.gui.utils.ui.DocumentUpdateListener;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginUpdate;
public class PluginsSettings {
private static final Logger LOG = LoggerFactory.getLogger(PluginsSettings.class);
@@ -44,12 +49,11 @@ public class PluginsSettings {
this.settings = settings;
}
public SettingsGroup build() {
SettingsGroup pluginsGroup = new SubSettingsGroup(NLS.str("preferences.plugins"));
fillMainSettings(pluginsGroup);
List<PluginContext> list = new CollectPluginOptions(mainWindow).build();
public ISettingsGroup build() {
List<PluginContext> list = new CollectPlugins(mainWindow).build();
ISettingsGroup pluginsGroup = new PluginsSettingsGroup(this, list);
for (PluginContext context : list) {
ISettingsGroup pluginGroup = buildPluginGroup(context);
ISettingsGroup pluginGroup = addPluginGroup(context);
if (pluginGroup != null) {
pluginsGroup.getSubGroups().add(pluginGroup);
}
@@ -57,12 +61,37 @@ public class PluginsSettings {
return pluginsGroup;
}
private void fillMainSettings(SettingsGroup settingsGroup) {
JPanel panel = settingsGroup.getPanel();
panel.add(new JPanel());
public void addPlugin() {
new InstallPluginDialog(mainWindow).setVisible(true);
}
private ISettingsGroup buildPluginGroup(PluginContext context) {
public void uninstall(String pluginId) {
mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.uninstalling"), () -> {
boolean success = JadxPluginsTools.getInstance().uninstall(pluginId);
if (success) {
LOG.debug("Uninstall complete");
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
} else {
LOG.debug("Uninstall failed");
}
});
}
void updateAll() {
mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.updating"), () -> {
List<JadxPluginUpdate> updates = JadxPluginsTools.getInstance().updateAll();
if (!updates.isEmpty()) {
LOG.debug("Updates: {}\n ", Utils.listToString(updates, "\n "));
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
} else {
LOG.debug("No updates found");
}
});
}
private ISettingsGroup addPluginGroup(PluginContext context) {
JadxGuiContext guiContext = context.getGuiContext();
if (guiContext instanceof GuiPluginContext) {
GuiPluginContext pluginGuiContext = ((GuiPluginContext) guiContext);
@@ -0,0 +1,211 @@
package jadx.gui.settings.ui.plugins;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils;
import jadx.gui.utils.NLS;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
class PluginsSettingsGroup implements ISettingsGroup {
private final PluginsSettings pluginsSettings;
private final String title;
private final List<ISettingsGroup> subGroups = new ArrayList<>();
private final List<PluginContext> pluginsList;
private PluginListNode selectedPlugin;
private JPanel detailsPanel;
public PluginsSettingsGroup(PluginsSettings pluginsSettings, List<PluginContext> pluginsList) {
this.pluginsSettings = pluginsSettings;
this.title = NLS.str("preferences.plugins");
this.pluginsList = pluginsList;
}
@Override
public String getTitle() {
return title;
}
@Override
public List<ISettingsGroup> getSubGroups() {
return subGroups;
}
@Override
public JComponent buildComponent() {
// lazy load main page
return buildMainSettingsPage();
}
private JPanel buildMainSettingsPage() {
JButton installPluginBtn = new JButton(NLS.str("preferences.plugins.install"));
installPluginBtn.addActionListener(ev -> pluginsSettings.addPlugin());
JButton updateAllBtn = new JButton(NLS.str("preferences.plugins.update_all"));
updateAllBtn.addActionListener(ev -> pluginsSettings.updateAll());
JPanel actionsPanel = new JPanel();
actionsPanel.setLayout(new BoxLayout(actionsPanel, BoxLayout.LINE_AXIS));
actionsPanel.add(installPluginBtn);
actionsPanel.add(Box.createRigidArea(new Dimension(5, 0)));
actionsPanel.add(updateAllBtn);
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
Map<String, JadxPluginMetadata> installedMap = new HashMap<>(installed.size());
installed.forEach(p -> installedMap.put(p.getPluginId(), p));
List<BasePluginListNode> nodes = new ArrayList<>(installed.size() + 3);
for (PluginContext plugin : pluginsList) {
nodes.add(new PluginListNode(plugin, installedMap.get(plugin.getPluginId())));
}
nodes.sort(Comparator.comparing(n -> n.getPluginInfo().getName()));
DefaultListModel<BasePluginListNode> listModel = new DefaultListModel<>();
listModel.addElement(new TitleNode("Installed"));
nodes.stream().filter(n -> n.getVersion() != null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Bundled"));
nodes.stream().filter(n -> n.getVersion() == null).forEach(listModel::addElement);
// TODO: load external plugins list
// listModel.addElement(new TitleNode("Available"));
JList<BasePluginListNode> pluginsList = new JList<>(listModel);
pluginsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
pluginsList.setCellRenderer(new PluginsListCellRenderer(pluginsList));
pluginsList.addListSelectionListener(ev -> onSelection(pluginsList.getSelectedValue()));
JScrollPane scrollPane = new JScrollPane(pluginsList);
detailsPanel = new JPanel(new BorderLayout(5, 5));
detailsPanel.setBorder(BorderFactory.createTitledBorder("Plugin details"));
detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.PAGE_AXIS));
detailsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JSplitPane splitPanel = new JSplitPane();
splitPanel.setBorder(BorderFactory.createEmptyBorder(10, 2, 2, 2));
splitPanel.setLeftComponent(scrollPane);
splitPanel.setRightComponent(detailsPanel);
splitPanel.setDividerLocation(0.4);
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout(5, 5));
mainPanel.setBorder(BorderFactory.createTitledBorder(title));
mainPanel.add(actionsPanel, BorderLayout.PAGE_START);
mainPanel.add(splitPanel, BorderLayout.CENTER);
return mainPanel;
}
private void onSelection(BasePluginListNode node) {
detailsPanel.removeAll();
JadxPluginInfo pluginInfo = node.getPluginInfo();
if (pluginInfo != null) {
JButton uninstallBtn = new JButton("Uninstall");
if (node.getVersion() != null) {
uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(pluginInfo.getPluginId()));
} else {
uninstallBtn.setEnabled(false);
}
JLabel nameLbl = new JLabel(pluginInfo.getName());
Font baseFont = nameLbl.getFont();
nameLbl.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize2D() + 2));
JTextPane descArea = new JTextPane();
descArea.setText(pluginInfo.getDescription());
descArea.setFont(baseFont.deriveFont(baseFont.getSize2D() + 1));
descArea.setEditable(false);
descArea.setBorder(BorderFactory.createEmptyBorder());
descArea.setOpaque(true);
JPanel top = new JPanel();
top.setLayout(new BoxLayout(top, BoxLayout.LINE_AXIS));
top.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2));
top.add(nameLbl);
top.add(Box.createHorizontalGlue());
top.add(uninstallBtn);
detailsPanel.add(top, BorderLayout.PAGE_START);
detailsPanel.add(descArea, BorderLayout.CENTER);
}
detailsPanel.updateUI();
}
private static class PluginsListCellRenderer implements ListCellRenderer<BasePluginListNode> {
private final JPanel panel;
private final JLabel nameLbl;
private final JLabel versionLbl;
private final JLabel titleLbl;
public PluginsListCellRenderer(JList<BasePluginListNode> pluginsList) {
panel = new JPanel();
panel.setOpaque(true);
panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
panel.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
nameLbl = new JLabel("");
nameLbl.setFont(nameLbl.getFont().deriveFont(Font.BOLD));
nameLbl.setOpaque(true);
versionLbl = new JLabel("");
versionLbl.setOpaque(true);
panel.add(nameLbl);
panel.add(Box.createHorizontalGlue());
panel.add(versionLbl);
titleLbl = new JLabel();
titleLbl.setHorizontalAlignment(SwingConstants.CENTER);
titleLbl.setEnabled(false);
}
@Override
public Component getListCellRendererComponent(JList<? extends BasePluginListNode> list,
BasePluginListNode value, int index, boolean isSelected, boolean cellHasFocus) {
String title = value.getTitle();
if (title != null) {
titleLbl.setText(title);
return titleLbl;
}
nameLbl.setText(value.getPluginInfo().getName());
nameLbl.setToolTipText(value.getPluginInfo().getDescription());
versionLbl.setText(Utils.getOrElse(value.getVersion(), ""));
if (isSelected) {
panel.setBackground(list.getSelectionBackground());
nameLbl.setBackground(list.getSelectionBackground());
nameLbl.setForeground(list.getSelectionForeground());
versionLbl.setBackground(list.getSelectionBackground());
} else {
panel.setBackground(list.getBackground());
nameLbl.setBackground(list.getBackground());
nameLbl.setForeground(list.getForeground());
versionLbl.setBackground(list.getBackground());
}
return panel;
}
}
}
@@ -0,0 +1,14 @@
package jadx.gui.settings.ui.plugins;
public class TitleNode extends BasePluginListNode {
private final String title;
public TitleNode(String title) {
this.title = title;
}
@Override
public String getTitle() {
return title;
}
}
@@ -104,6 +104,7 @@ import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.JadxSettingsWindow;
import jadx.gui.settings.ui.plugins.InstallPluginDialog;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JLoadableNode;
@@ -482,6 +483,7 @@ public class MainWindow extends JFrame {
private void loadFiles(Runnable onFinish) {
if (project.getFilePaths().isEmpty()) {
tabbedPane.showNode(new StartPageNode());
return;
}
AtomicReference<Exception> wrapperException = new AtomicReference<>();
@@ -1123,7 +1125,7 @@ public class MainWindow extends JFrame {
pluginsMenu = new JMenu(NLS.str("menu.plugins"));
pluginsMenu.setMnemonic(KeyEvent.VK_P);
pluginsMenu.setVisible(false);
resetPluginsMenu();
JMenu tools = new JMenu(NLS.str("menu.tools"));
tools.setMnemonic(KeyEvent.VK_T);
@@ -1639,6 +1641,19 @@ public class MainWindow extends JFrame {
return pluginsMenu;
}
public void resetPluginsMenu() {
pluginsMenu.removeAll();
pluginsMenu.add(new ActionHandler(() -> new InstallPluginDialog(this).setVisible(true))
.withNameAndDesc(NLS.str("preferences.plugins.install")));
}
public void addToPluginsMenu(Action item) {
if (pluginsMenu.getMenuComponentCount() == 1) {
pluginsMenu.addSeparator();
}
pluginsMenu.add(item);
}
public RenameMappingsGui getRenameMappings() {
return renameMappings;
}
@@ -40,8 +40,8 @@ public class LafManager {
public static void updateLaf(JadxSettings settings) {
if (setupLaf(getThemeClass(settings))) {
// update all components
FlatLaf.updateUI();
FlatAnimatedLafChange.hideSnapshotWithAnimation();
FlatLaf.updateUI();
}
}
@@ -1,9 +1,9 @@
package jadx.gui.utils.plugins;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
@@ -15,15 +15,15 @@ import jadx.gui.ui.MainWindow;
import jadx.plugins.tools.JadxExternalPluginsLoader;
/**
* Collect options from all plugins.
* Collect all plugins.
* Init not yet loaded plugins in new temporary context.
* Support case if decompiler in wrapper not initialized yet.
*/
public class CollectPluginOptions {
public class CollectPlugins {
private final MainWindow mainWindow;
public CollectPluginOptions(MainWindow mainWindow) {
public CollectPlugins(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
@@ -50,9 +50,6 @@ public class CollectPluginOptions {
pluginManager.init(missingPlugins);
allPlugins.addAll(missingPlugins);
}
return allPlugins.stream()
.filter(context -> context.getOptions() != null)
.sorted()
.collect(Collectors.toList());
return new ArrayList<>(allPlugins);
}
}
@@ -222,6 +222,16 @@ preferences.search_group_title=Ressourcen durchsuchen
preferences.res_file_ext=Dateierweiterungen (z.B. .xml|.html), * bedeutet alle
preferences.res_skip_file=Dateien überspringen (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=Bitte Datei öffnen
msg.saving_sources=Quelltexte speichern
msg.language_changed_title=Sprache speichern
@@ -222,6 +222,16 @@ preferences.search_results_per_page=Results per page (0 - no limit)
preferences.res_file_ext=Resource files extensions ('xml|html', * for all)
preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable)
preferences.plugins.install=Install plugin
preferences.plugins.install_btn=Install
preferences.plugins.location_id_label=Location id:
preferences.plugins.plugin_jar=Select Plugin jar
preferences.plugins.plugin_jar_label=or
preferences.plugins.update_all=Update All
preferences.plugins.task.installing=Installing plugin
preferences.plugins.task.uninstalling=Uninstalling plugin
preferences.plugins.task.updating=Updating plugins
msg.open_file=Please open file
msg.saving_sources=Saving sources
msg.language_changed_title=Language changed
@@ -222,6 +222,16 @@ preferences.reset_title=Reestablecer preferencias
#preferences.res_file_ext=Resource files extensions ('xml|html', * for all)
#preferences.res_skip_file=Skip resources files if larger (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=Por favor, abra un archivo
msg.saving_sources=Guardando fuente
msg.language_changed_title=Idioma cambiado
@@ -222,6 +222,16 @@ preferences.search_group_title=리소스 검색
preferences.res_file_ext=파일 확장자 (예: .xml|.html) (* 은 전체를 의미)
preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=파일을 여십시오
msg.saving_sources=소스 저장 중
msg.language_changed_title=언어 변경됨
@@ -222,6 +222,16 @@ preferences.search_group_title=Buscar recursos
preferences.res_file_ext=Extensões de arquivos (ex: .xml|.html), * significa todas
preferences.res_skip_file=Pular arquivos excedidos
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=Abra um arquivo
msg.saving_sources=Salvando recursos
msg.language_changed_title=Idioma alterado
@@ -222,6 +222,16 @@ preferences.search_results_per_page=Результатов на страницу
preferences.res_file_ext=Расширения файлов ресурсов ('xml|html', * для всех)
preferences.res_skip_file=Пропускать ресурсы больше чем (в МБ)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=Пожалуйста, откройте файл
msg.saving_sources=Сохранение ресурсов
msg.language_changed_title=Язык изменен
@@ -222,6 +222,16 @@ preferences.search_results_per_page=每页结果数(0 - 无限制)
preferences.res_file_ext=文件扩展名(e.g. .xml|.html),* 表示所有
preferences.res_skip_file=跳过文件大小(MB
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=请打开文件
msg.saving_sources=正在导出源代码
msg.language_changed_title=语言已更改
@@ -222,6 +222,16 @@ preferences.search_results_per_page=每頁的搜尋結果數 (0 - 無限制)
preferences.res_file_ext=副檔名 (e.g. .xml|.html), * 表示全部
preferences.res_skip_file=略過大於此值的檔案 (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
msg.open_file=請開啟檔案
msg.saving_sources=正在儲存原始碼
msg.language_changed_title=已更改語言
@@ -4,7 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -46,7 +46,7 @@ public class JadxPluginsTools {
private JadxPluginsTools() {
ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx");
Path plugins = Paths.get(jadxDirs.configDir, "plugins"); // TODO: use dataDir?
Path plugins = Paths.get(jadxDirs.configDir, "plugins");
makeDirs(plugins);
pluginsJson = plugins.resolve("plugins.json");
dropins = plugins.resolve("dropins");
@@ -165,7 +165,9 @@ public class JadxPluginsTools {
}
fillPluginInfoFromJar(metadata, tmpJar);
Path pluginJar = installed.resolve(metadata.getPluginId() + '-' + metadata.getVersion() + ".jar");
String version = metadata.getVersion();
String fileName = metadata.getPluginId() + (version != null ? '-' + version : "") + ".jar";
Path pluginJar = installed.resolve(fileName);
copyJar(tmpJar, pluginJar);
metadata.setJar(installed.relativize(pluginJar).toString());
@@ -195,7 +197,7 @@ public class JadxPluginsTools {
}
private void downloadJar(String sourceJar, Path destPath) {
try (InputStream in = new URL(sourceJar).openStream()) {
try (InputStream in = URI.create(sourceJar).toURL().openStream()) {
Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Failed to download jar: " + sourceJar, e);
@@ -1,12 +1,13 @@
package jadx.plugins.tools.data;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
private String pluginId;
private String name;
private String description;
private String version;
private @Nullable String version;
private String locationId;
private String resolverId;
private String jar;
@@ -27,7 +28,7 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
this.name = name;
}
public String getVersion() {
public @Nullable String getVersion() {
return version;
}
@@ -93,7 +94,7 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
return "JadxPluginMetadata{"
+ "id=" + pluginId
+ ", name=" + name
+ ", version=" + version
+ ", version=" + (version != null ? version : "?")
+ ", locationId=" + locationId
+ ", jar=" + jar
+ '}';
@@ -10,10 +10,9 @@ Examples: `github:skylot:jadx`, `github:skylot:jadx:sample-plugin` or `github:sk
`<version>` - exact version to install (optional), should be equal to release name
Artifact should have a name: `<artifact name prefix>-<release-version-name>.jar`.
Artifact name pattern: `<artifact name prefix>[-<release-version-name>].jar`.
Default value for `<artifact name prefix>` is a repo name,
`release-version-name` should have a `x.x.x` format.
Default value for `<artifact name prefix>` is a repo name, `-<release-version-name>` is optional.
---
@@ -64,7 +64,7 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
Asset asset = release.getAssets().stream()
.filter(a -> a.getName().equals(artifactName))
.findFirst()
.orElseThrow(() -> new RuntimeException("Release artifact with name '" + artifactName + "' not found"));
.orElse(searchIgnoringVersion(release, artifactPrefix));
JadxPluginMetadata metadata = new JadxPluginMetadata();
metadata.setResolverId(id());
@@ -74,6 +74,13 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
return Optional.of(metadata);
}
private @NotNull Asset searchIgnoringVersion(Release release, String artifactPrefix) {
return release.getAssets().stream()
.filter(a -> a.getName().startsWith(artifactPrefix) && a.getName().startsWith(".jar"))
.findFirst()
.orElseThrow(() -> new RuntimeException("Release artifact with prefix '" + artifactPrefix + "' not found"));
}
@NotNull
private static String buildLocationId(String owner, String project, String artifactPrefix) {
if (project.equals(artifactPrefix)) {
@@ -38,7 +38,7 @@ public class RenameMappingsOptions extends BaseOptionsParser {
public List<OptionDescription> getOptionsDescriptions() {
return Arrays.asList(
new JadxOptionDescription(FORMAT_OPT, "mapping format", "auto", getMappingFormats())
.withFlag(OptionFlag.PER_PROJECT),
.withFlags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI),
JadxOptionDescription.booleanOption(INVERT_OPT, "invert mapping", false)
.withFlag(OptionFlag.PER_PROJECT));
}
@@ -4,7 +4,14 @@ val jadx = getJadxInstance()
jadx.args.isDeobfuscationOn = false
jadx.args.renameFlags = emptySet()
val regex = """[Oo0]+""".toRegex()
val regexOpt = jadx.options.registerString(
"regex",
"Apply rename for names matches regex",
values = listOf(),
defaultValue = "[Oo0]+",
)
val regex = regexOpt.value.toRegex()
var n = 0
jadx.rename.all { name, node ->
when {
@@ -13,7 +13,7 @@ object JadxScriptOptionsUI {
.groupBy { it.script }
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
.toList()
settings.setCustomSettings(EmptyRootGroup("Scripts", subGroups))
settings.setCustomSettingsGroup(EmptyRootGroup("Scripts", subGroups))
}
}
@@ -1,6 +1,5 @@
package jadx.plugins.script
import jadx.api.JadxDecompiler
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.runtime.JadxScriptData
import jadx.plugins.script.runtime.JadxScriptTemplate
@@ -18,13 +17,15 @@ import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate
import kotlin.system.measureTimeMillis
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class ScriptEval {
private val scriptingHost = BasicJvmScriptingHost()
fun process(init: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
val jadx = init.decompiler as JadxDecompiler
val jadx = init.decompiler
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
if (scripts.isEmpty()) {
return emptyList()
@@ -60,7 +61,7 @@ class ScriptEval {
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compilationConf, evalConf)
processEvalResult(result, scriptData)
}
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in $execTime ms" }
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
}
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, scriptData: JadxScriptData) {