diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 5e7f47984..782acab59 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -12,7 +12,9 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +33,7 @@ import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.core.deobf.DeobfAliasProvider; import jadx.core.deobf.DeobfCondition; +import jadx.core.plugins.PluginContext; import jadx.core.utils.files.FileUtils; public class JadxArgs implements Closeable { @@ -652,7 +655,7 @@ public class JadxArgs implements Closeable { /** * Hash of all options that can change result code */ - public String makeCodeArgsHash() { + public String makeCodeArgsHash(@Nullable JadxDecompiler decompiler) { String argStr = "args:" + decompilationMode + useImports + showInconsistentCode + inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda + deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength @@ -661,10 +664,21 @@ public class JadxArgs implements Closeable { + insertDebugLines + extractFinally + debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts + respectBytecodeAccModifiers + fsCaseSensitive + renameFlags - + commentsLevel + useDxInput + integerFormat; + + commentsLevel + useDxInput + integerFormat + + "|" + buildPluginsHash(decompiler); return FileUtils.md5Sum(argStr); } + private static String buildPluginsHash(@Nullable JadxDecompiler decompiler) { + if (decompiler == null) { + return ""; + } + return decompiler.getPluginManager().getResolvedPluginContexts() + .stream() + .map(PluginContext::getInputsHash) + .collect(Collectors.joining(":")); + } + @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java index 8ceb1a50c..4e11eb364 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java @@ -26,8 +26,8 @@ public interface JadxPluginContext { /** * Function to calculate hash of all options which can change output code. - * Hash for input files ({@link JadxArgs#getInputFiles()}) already calculated, - * so this method can omit these files. + * Hash for input files ({@link JadxArgs#getInputFiles()}) and registered options + * calculated by default implementations. */ void registerInputsHashSupplier(Supplier supplier); diff --git a/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java b/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java new file mode 100644 index 000000000..8a8539331 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java @@ -0,0 +1,29 @@ +package jadx.api.plugins.gui; + +import java.util.Collections; +import java.util.List; + +import javax.swing.JComponent; + +/** + * Settings page customization + */ +public interface ISettingsGroup { + + /** + * Node name + */ + String getTitle(); + + /** + * Custom page component + */ + JComponent buildComponent(); + + /** + * Optional child nodes list + */ + default List getSubGroups() { + return Collections.emptyList(); + } +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java index 46cd7ed27..1fbfb0baa 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java +++ b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java @@ -44,4 +44,9 @@ public interface JadxGuiContext { boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action); void copyToClipboard(String str); + + /** + * Access to GUI settings + */ + JadxGuiSettings settings(); } diff --git a/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiSettings.java b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiSettings.java new file mode 100644 index 000000000..5454c3132 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiSettings.java @@ -0,0 +1,18 @@ +package jadx.api.plugins.gui; + +import java.util.List; + +import jadx.api.plugins.options.OptionDescription; + +public interface JadxGuiSettings { + + /** + * Set plugin custom settings page + */ + void setCustomSettings(ISettingsGroup group); + + /** + * Helper method to build options group only for provided option list + */ + ISettingsGroup buildSettingsGroupForOptions(String title, List options); +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java b/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java index 7d1c822ca..2808fcea4 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java +++ b/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java @@ -25,19 +25,10 @@ public interface OptionDescription { @Nullable String defaultValue(); - enum OptionType { - STRING, NUMBER, BOOLEAN - } - default OptionType getType() { return OptionType.STRING; } - enum OptionFlag { - PER_PROJECT, // store in project settings instead global (for jadx-gui) - HIDE_IN_GUI, // do not show this option in jadx-gui (useful if option is configured with custom ui) - } - default Set getFlags() { return Collections.emptySet(); } diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/OptionFlag.java b/jadx-core/src/main/java/jadx/api/plugins/options/OptionFlag.java new file mode 100644 index 000000000..d36b18315 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/options/OptionFlag.java @@ -0,0 +1,24 @@ +package jadx.api.plugins.options; + +public enum OptionFlag { + /** + * Store in project settings instead global (for jadx-gui) + */ + PER_PROJECT, + + /** + * Do not show this option in jadx-gui (useful if option is configured with custom ui) + */ + HIDE_IN_GUI, + + /** + * Do not show this option in jadx-gui (useful if option is configured with custom ui) + */ + DISABLE_IN_GUI, + + /** + * Add this flag only if option do not affect generated code. + * If added, option value change will not cause code cache reset. + */ + NOT_CHANGING_CODE, +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/OptionType.java b/jadx-core/src/main/java/jadx/api/plugins/options/OptionType.java new file mode 100644 index 000000000..c6480cbd2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/options/OptionType.java @@ -0,0 +1,7 @@ +package jadx.api.plugins.options; + +public enum OptionType { + STRING, + NUMBER, + BOOLEAN +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java b/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java index 12b892543..695a8802b 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java +++ b/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java @@ -9,6 +9,8 @@ import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.options.OptionDescription; +import jadx.api.plugins.options.OptionFlag; +import jadx.api.plugins.options.OptionType; public class JadxOptionDescription implements OptionDescription { diff --git a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java index 6728bf54d..b30b7bb3b 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java @@ -7,15 +7,14 @@ import java.util.Objects; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.stream.Collectors; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPlugin; -import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.plugins.options.JadxPluginOptions; @@ -30,7 +29,7 @@ public class JadxPluginManager { private final SortedSet resolvedPlugins = new TreeSet<>(); private final Map provideSuggestions = new TreeMap<>(); - private @Nullable JadxGuiContext guiContext; + private final List> addPluginListeners = new ArrayList<>(); public JadxPluginManager(JadxDecompiler decompiler) { this.decompiler = decompiler; @@ -64,7 +63,7 @@ public class JadxPluginManager { if (!allPlugins.add(pluginContext)) { throw new IllegalArgumentException("Duplicate plugin id: " + pluginContext + ", class " + plugin.getClass()); } - pluginContext.setGuiContext(guiContext); + addPluginListeners.forEach(l -> l.accept(pluginContext)); return pluginContext; } @@ -166,10 +165,9 @@ public class JadxPluginManager { .collect(Collectors.toList()); } - public void setGuiContext(JadxGuiContext guiContext) { - this.guiContext = guiContext; - for (PluginContext context : getAllPluginContexts()) { - context.setGuiContext(guiContext); - } + public void registerAddPluginListener(Consumer listener) { + this.addPluginListeners.add(listener); + // run for already added plugins + getAllPluginContexts().forEach(listener); } } diff --git a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java index 86dd08154..92120b2e3 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java +++ b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java @@ -2,6 +2,7 @@ package jadx.core.plugins; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Supplier; @@ -16,8 +17,11 @@ import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; +import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.pass.JadxPass; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; public class PluginContext implements JadxPluginContext, Comparable { private final JadxDecompiler decompiler; @@ -86,14 +90,28 @@ public class PluginContext implements JadxPluginContext, Comparable allOptions = getArgs().getPluginOptions(); + StringBuilder sb = new StringBuilder(); + for (OptionDescription optDesc : options.getOptionsDescriptions()) { + if (!optDesc.getFlags().contains(OptionFlag.NOT_CHANGING_CODE)) { + sb.append(':').append(allOptions.get(optDesc.name())); } } - return ""; + return FileUtils.md5Sum(sb.toString()); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 2569c4eaa..e199d198d 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -31,7 +31,8 @@ import jadx.gui.cache.code.CodeStringCache; import jadx.gui.cache.code.disk.BufferCodeCache; import jadx.gui.cache.code.disk.DiskCodeCache; import jadx.gui.cache.usage.UsageInfoCache; -import jadx.gui.plugins.context.GuiPluginsContext; +import jadx.gui.plugins.context.CommonGuiPluginsContext; +import jadx.gui.plugins.context.GuiPluginContext; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; @@ -50,7 +51,7 @@ public class JadxWrapper { private final MainWindow mainWindow; private volatile @Nullable JadxDecompiler decompiler; - private GuiPluginsContext guiPluginsContext; + private CommonGuiPluginsContext guiPluginsContext; public JadxWrapper(MainWindow mainWindow) { this.mainWindow = mainWindow; @@ -139,11 +140,14 @@ public class JadxWrapper { } private void initGuiPluginsContext() { - guiPluginsContext = new GuiPluginsContext(mainWindow); - decompiler.getPluginManager().setGuiContext(guiPluginsContext); + guiPluginsContext = new CommonGuiPluginsContext(mainWindow); + decompiler.getPluginManager().registerAddPluginListener(pluginContext -> { + GuiPluginContext guiContext = guiPluginsContext.buildForPlugin(pluginContext); + pluginContext.setGuiContext(guiContext); + }); } - public GuiPluginsContext getGuiPluginsContext() { + public CommonGuiPluginsContext getGuiPluginsContext() { return guiPluginsContext; } @@ -291,8 +295,7 @@ public class JadxWrapper { } /** - * @param fullName - * Full name of an outer class. Inner classes are not supported. + * @param fullName Full name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) { return getDecompiler().getClasses().stream() @@ -306,8 +309,7 @@ public class JadxWrapper { } /** - * @param rawName - * Full raw name of an outer class. Inner classes are not supported. + * @param rawName Full raw name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByRawName(String rawName) { return getDecompiler().getClasses().stream() diff --git a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java index 96fea2112..a00c4a146 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java @@ -20,7 +20,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -33,7 +32,6 @@ import jadx.api.JadxDecompiler; import jadx.core.Jadx; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.plugins.PluginContext; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; @@ -204,19 +202,8 @@ public class DiskCodeCache implements ICodeCache { } return DATA_FORMAT_VERSION + ":" + Jadx.getVersion() - + ":" + args.makeCodeArgsHash() - + ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath)) - + ":" + FileUtils.md5Sum(buildPluginsHash(decompiler)); - } - - private String buildPluginsHash(JadxDecompiler decompiler) { - if (decompiler == null) { - return ""; - } - return decompiler.getPluginManager().getResolvedPluginContexts() - .stream() - .map(PluginContext::getInputsHash) - .collect(Collectors.joining()); + + ":" + args.makeCodeArgsHash(decompiler) + + ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath)); } private int getClsId(String clsFullName) { diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/CommonGuiPluginsContext.java similarity index 50% rename from jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java rename to jadx-gui/src/main/java/jadx/gui/plugins/context/CommonGuiPluginsContext.java index e897bca4d..2f65d0e00 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/CommonGuiPluginsContext.java @@ -1,37 +1,44 @@ package jadx.gui.plugins.context; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; +import java.util.Map; import javax.swing.JMenu; -import javax.swing.JPanel; -import javax.swing.KeyStroke; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.metadata.ICodeNodeRef; -import jadx.api.plugins.gui.JadxGuiContext; +import jadx.core.plugins.PluginContext; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.JNodePopupBuilder; -import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.ActionHandler; -public class GuiPluginsContext implements JadxGuiContext { - private static final Logger LOG = LoggerFactory.getLogger(GuiPluginsContext.class); +public class CommonGuiPluginsContext { + private static final Logger LOG = LoggerFactory.getLogger(CommonGuiPluginsContext.class); private final MainWindow mainWindow; + private final Map pluginsMap = new HashMap<>(); private final List codePopupActionList = new ArrayList<>(); - public GuiPluginsContext(MainWindow mainWindow) { + public CommonGuiPluginsContext(MainWindow mainWindow) { this.mainWindow = mainWindow; } + public GuiPluginContext buildForPlugin(PluginContext pluginContext) { + GuiPluginContext guiPluginContext = new GuiPluginContext(this, pluginContext); + pluginsMap.put(pluginContext, guiPluginContext); + return guiPluginContext; + } + + public @Nullable GuiPluginContext getPluginGuiContext(PluginContext pluginContext) { + return pluginsMap.get(pluginContext); + } + public void reset() { codePopupActionList.clear(); JMenu pluginsMenu = mainWindow.getPluginsMenu(); @@ -39,12 +46,14 @@ public class GuiPluginsContext implements JadxGuiContext { pluginsMenu.setVisible(false); } - @Override - public void uiRun(Runnable runnable) { - UiUtils.uiRun(runnable); + public MainWindow getMainWindow() { + return mainWindow; + } + + public List getCodePopupActionList() { + return codePopupActionList; } - @Override public void addMenuAction(String name, Runnable action) { ActionHandler item = new ActionHandler(ev -> { try { @@ -59,12 +68,6 @@ public class GuiPluginsContext implements JadxGuiContext { pluginsMenu.setVisible(true); } - @Override - public void addPopupMenuAction(String name, @Nullable Function enabled, - @Nullable String keyBinding, Consumer action) { - codePopupActionList.add(new CodePopupAction(name, enabled, keyBinding, action)); - } - public void appendPopupMenus(CodeArea codeArea, JNodePopupBuilder popup) { if (codePopupActionList.isEmpty()) { return; @@ -74,24 +77,4 @@ public class GuiPluginsContext implements JadxGuiContext { popup.add(codePopupAction.buildAction(codeArea)); } } - - @Override - public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) { - KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding); - if (keyStroke == null) { - throw new IllegalArgumentException("Failed to parse key binding: " + keyBinding); - } - JPanel mainPanel = (JPanel) mainWindow.getContentPane(); - Object prevBinding = mainPanel.getInputMap().get(keyStroke); - if (prevBinding != null) { - return false; - } - UiUtils.addKeyBinding(mainPanel, keyStroke, id, action); - return true; - } - - @Override - public void copyToClipboard(String str) { - UiUtils.copyToClipboard(str); - } } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginContext.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginContext.java new file mode 100644 index 000000000..487768ea7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginContext.java @@ -0,0 +1,89 @@ +package jadx.gui.plugins.context; + +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.swing.JPanel; +import javax.swing.KeyStroke; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.plugins.gui.ISettingsGroup; +import jadx.api.plugins.gui.JadxGuiContext; +import jadx.api.plugins.gui.JadxGuiSettings; +import jadx.core.plugins.PluginContext; +import jadx.gui.utils.UiUtils; + +public class GuiPluginContext implements JadxGuiContext { + private static final Logger LOG = LoggerFactory.getLogger(GuiPluginContext.class); + + private final CommonGuiPluginsContext commonContext; + private final PluginContext pluginContext; + + private @Nullable ISettingsGroup customSettingsGroup; + + public GuiPluginContext(CommonGuiPluginsContext commonContext, PluginContext pluginContext) { + this.commonContext = commonContext; + this.pluginContext = pluginContext; + } + + public CommonGuiPluginsContext getCommonContext() { + return commonContext; + } + + public PluginContext getPluginContext() { + return pluginContext; + } + + @Override + public void uiRun(Runnable runnable) { + UiUtils.uiRun(runnable); + } + + @Override + public void addMenuAction(String name, Runnable action) { + commonContext.addMenuAction(name, action); + } + + @Override + public void addPopupMenuAction(String name, @Nullable Function enabled, + @Nullable String keyBinding, Consumer action) { + commonContext.getCodePopupActionList().add(new CodePopupAction(name, enabled, keyBinding, action)); + } + + @Override + public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) { + KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding); + if (keyStroke == null) { + throw new IllegalArgumentException("Failed to parse key binding: " + keyBinding); + } + JPanel mainPanel = (JPanel) commonContext.getMainWindow().getContentPane(); + Object prevBinding = mainPanel.getInputMap().get(keyStroke); + if (prevBinding != null) { + return false; + } + UiUtils.addKeyBinding(mainPanel, keyStroke, id, action); + return true; + } + + @Override + public void copyToClipboard(String str) { + UiUtils.copyToClipboard(str); + } + + @Override + public JadxGuiSettings settings() { + return new GuiSettingsContext(this); + } + + void setCustomSettings(ISettingsGroup customSettingsGroup) { + this.customSettingsGroup = customSettingsGroup; + } + + public @Nullable ISettingsGroup getCustomSettingsGroup() { + return customSettingsGroup; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java new file mode 100644 index 000000000..9aa7837f8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java @@ -0,0 +1,32 @@ +package jadx.gui.plugins.context; + +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.ui.MainWindow; + +public class GuiSettingsContext implements JadxGuiSettings { + private final GuiPluginContext guiPluginContext; + + public GuiSettingsContext(GuiPluginContext guiPluginContext) { + this.guiPluginContext = guiPluginContext; + } + + @Override + public void setCustomSettings(ISettingsGroup group) { + guiPluginContext.setCustomSettings(group); + } + + @Override + public ISettingsGroup buildSettingsGroupForOptions(String title, List options) { + MainWindow mainWindow = guiPluginContext.getCommonContext().getMainWindow(); + PluginsSettings pluginsSettings = new PluginsSettings(mainWindow, mainWindow.getSettings()); + SubSettingsGroup settingsGroup = new SubSettingsGroup(title); + pluginsSettings.addOptions(settingsGroup, options); + return settingsGroup; + } +} 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 b1d63e9f9..992c73b13 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 @@ -15,9 +15,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.Box; @@ -53,16 +50,13 @@ import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; +import jadx.api.JadxDecompiler; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.args.IntegerFormat; import jadx.api.args.ResourceNameSource; -import jadx.api.plugins.options.JadxPluginOptions; -import jadx.api.plugins.options.OptionDescription; -import jadx.api.plugins.options.OptionDescription.OptionFlag; -import jadx.core.plugins.PluginContext; +import jadx.api.plugins.gui.ISettingsGroup; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.usage.UsageCacheMode; -import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsAdapter; import jadx.gui.settings.LineNumbersMode; @@ -73,7 +67,6 @@ import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.plugins.CollectPluginOptions; import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.DocumentUpdateListener; @@ -85,6 +78,7 @@ public class JadxSettingsWindow extends JDialog { private final transient MainWindow mainWindow; private final transient JadxSettings settings; private final transient String startSettings; + private final transient String startSettingsHash; private final transient LangLocale prevLang; private transient boolean needReload = false; @@ -93,6 +87,7 @@ public class JadxSettingsWindow extends JDialog { this.mainWindow = mainWindow; this.settings = settings; this.startSettings = JadxSettingsAdapter.makeString(settings); + this.startSettingsHash = calcSettingsHash(); this.prevLang = settings.getLangLocale(); initUI(); @@ -113,14 +108,14 @@ public class JadxSettingsWindow extends JDialog { groupPanel.setLayout(new BoxLayout(groupPanel, BoxLayout.LINE_AXIS)); groupPanel.setBorder(BorderFactory.createEmptyBorder(10, 3, 3, 10)); - List groups = new ArrayList<>(); + List groups = new ArrayList<>(); groups.add(makeDecompilationGroup()); groups.add(makeDeobfuscationGroup()); groups.add(makeRenameGroup()); groups.add(makeAppearanceGroup()); groups.add(makeSearchResGroup()); groups.add(makeProjectGroup()); - groups.add(makePluginOptionsGroup()); + groups.add(new PluginsSettings(mainWindow, settings).build()); groups.add(makeOtherGroup()); SettingsTree tree = new SettingsTree(); @@ -187,7 +182,7 @@ public class JadxSettingsWindow extends JDialog { } } - private SettingsGroupPanel makeDeobfuscationGroup() { + private SettingsGroup makeDeobfuscationGroup() { JCheckBox deobfOn = new JCheckBox(); deobfOn.setSelected(settings.isDeobfuscationOn()); deobfOn.addItemListener(e -> { @@ -228,7 +223,7 @@ public class JadxSettingsWindow extends JDialog { } }); - SettingsGroupPanel deobfGroup = new SettingsGroupPanel(NLS.str("preferences.deobfuscation")); + SettingsGroup deobfGroup = new SettingsGroup(NLS.str("preferences.deobfuscation")); deobfGroup.addRow(NLS.str("preferences.deobfuscation_on"), deobfOn); deobfGroup.addRow(NLS.str("preferences.deobfuscation_min_len"), minLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner); @@ -242,7 +237,7 @@ public class JadxSettingsWindow extends JDialog { return deobfGroup; } - private SettingsGroupPanel makeRenameGroup() { + private SettingsGroup makeRenameGroup() { JCheckBox renameCaseSensitive = new JCheckBox(); renameCaseSensitive.setSelected(settings.isRenameCaseSensitive()); renameCaseSensitive.addItemListener(e -> { @@ -271,7 +266,7 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); - SettingsGroupPanel group = new SettingsGroupPanel(NLS.str("preferences.rename")); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.rename")); group.addRow(NLS.str("preferences.rename_case"), renameCaseSensitive); group.addRow(NLS.str("preferences.rename_valid"), renameValid); group.addRow(NLS.str("preferences.rename_printable"), renamePrintable); @@ -283,18 +278,18 @@ public class JadxSettingsWindow extends JDialog { connectedComponents.forEach(comp -> comp.setEnabled(enabled)); } - private SettingsGroupPanel makeProjectGroup() { + private SettingsGroup makeProjectGroup() { JCheckBox autoSave = new JCheckBox(); autoSave.setSelected(settings.isAutoSaveProject()); autoSave.addItemListener(e -> settings.setAutoSaveProject(e.getStateChange() == ItemEvent.SELECTED)); - SettingsGroupPanel group = new SettingsGroupPanel(NLS.str("preferences.project")); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.project")); group.addRow(NLS.str("preferences.autoSave"), autoSave); return group; } - private SettingsGroupPanel makeAppearanceGroup() { + private SettingsGroup makeAppearanceGroup() { JComboBox languageCbx = new JComboBox<>(NLS.getLangLocales()); for (LangLocale locale : NLS.getLangLocales()) { if (locale.equals(settings.getLangLocale())) { @@ -329,7 +324,7 @@ public class JadxSettingsWindow extends JDialog { mainWindow.loadSettings(); }); - SettingsGroupPanel group = new SettingsGroupPanel(NLS.str("preferences.appearance")); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.appearance")); group.addRow(NLS.str("preferences.language"), languageCbx); group.addRow(NLS.str("preferences.laf_theme"), lafCbx); group.addRow(NLS.str("preferences.theme"), themesCbx); @@ -382,7 +377,7 @@ public class JadxSettingsWindow extends JDialog { return NLS.str("preferences.smali_font") + ": " + font.getFontName() + ' ' + fontStyleName + ' ' + font.getSize(); } - private SettingsGroupPanel makeDecompilationGroup() { + private SettingsGroup makeDecompilationGroup() { JCheckBox useDx = new JCheckBox(); useDx.setSelected(settings.isUseDx()); useDx.addItemListener(e -> { @@ -552,7 +547,7 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); - SettingsGroupPanel other = new SettingsGroupPanel(NLS.str("preferences.decompile")); + SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile")); other.addRow(NLS.str("preferences.threads"), threadsCount); other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"), editExcludedPackages); @@ -580,109 +575,7 @@ public class JadxSettingsWindow extends JDialog { return other; } - private SettingsGroupPanel makePluginOptionsGroup() { - SettingsGroupPanel pluginsGroup = new SettingsGroupPanel(NLS.str("preferences.plugins")); - List list = new CollectPluginOptions(mainWindow.getWrapper()).build(); - for (PluginContext context : list) { - addPluginOptions(pluginsGroup, context); - } - return pluginsGroup; - } - - private void addPluginOptions(SettingsGroupPanel pluginsGroup, PluginContext context) { - JadxPluginOptions options = context.getOptions(); - if (options == null) { - return; - } - String pluginId = context.getPluginId(); - for (OptionDescription opt : options.getOptionsDescriptions()) { - if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) { - continue; - } - String optName = opt.name(); - String title; - if (pluginId.equals("jadx-script")) { - title = '[' + optName.replace("jadx-script.", "script:") + "] " + opt.description(); - } else { - title = '[' + pluginId + "] " + opt.description(); - } - Consumer updateFunc; - String curValue; - if (opt.getFlags().contains(OptionFlag.PER_PROJECT)) { - JadxProject project = mainWindow.getProject(); - updateFunc = value -> project.updatePluginOptions(m -> m.put(optName, value)); - curValue = project.getPluginOption(optName); - } else { - Map optionsMap = settings.getPluginOptions(); - updateFunc = value -> optionsMap.put(optName, value); - curValue = optionsMap.get(optName); - } - String value = curValue != null ? curValue : opt.defaultValue(); - - if (opt.values().isEmpty() || opt.getType() == OptionDescription.OptionType.BOOLEAN) { - try { - pluginsGroup.addRow(title, getPluginOptionEditor(opt, value, updateFunc)); - } catch (Exception e) { - LOG.error("Failed to add editor for plugin option: {}", optName, e); - } - } else { - JComboBox combo = new JComboBox<>(opt.values().toArray(new String[0])); - combo.setSelectedItem(value); - combo.addActionListener(e -> { - updateFunc.accept((String) combo.getSelectedItem()); - needReload(); - }); - pluginsGroup.addRow(title, combo); - } - } - } - - private JComponent getPluginOptionEditor(OptionDescription opt, String value, Consumer updateFunc) { - switch (opt.getType()) { - case STRING: - JTextField textField = new JTextField(); - textField.setText(value == null ? "" : value); - textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> { - updateFunc.accept(textField.getText()); - needReload(); - })); - return textField; - - case NUMBER: - JSpinner numberField = new JSpinner(); - numberField.setValue(safeStringToInt(value, 0)); - numberField.addChangeListener(e -> { - updateFunc.accept(numberField.getValue().toString()); - needReload(); - }); - return numberField; - - case BOOLEAN: - JCheckBox boolField = new JCheckBox(); - boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true")); - boolField.addItemListener(e -> { - boolean editorValue = e.getStateChange() == ItemEvent.SELECTED; - updateFunc.accept(editorValue ? "yes" : "no"); - needReload(); - }); - return boolField; - } - return null; - } - - private int safeStringToInt(String value, int defValue) { - if (value == null) { - return defValue; - } - try { - return Integer.parseInt(value); - } catch (Exception e) { - LOG.warn("Failed parse string to int: {}", value, e); - return defValue; - } - } - - private SettingsGroupPanel makeOtherGroup() { + private SettingsGroup makeOtherGroup() { JComboBox lineNumbersMode = new JComboBox<>(LineNumbersMode.values()); lineNumbersMode.setSelectedItem(settings.getLineNumbersMode()); lineNumbersMode.addActionListener(e -> { @@ -716,7 +609,7 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); - SettingsGroupPanel group = new SettingsGroupPanel(NLS.str("preferences.other")); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.other")); group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode); group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick); group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog); @@ -726,7 +619,7 @@ public class JadxSettingsWindow extends JDialog { return group; } - private SettingsGroupPanel makeSearchResGroup() { + private SettingsGroup makeSearchResGroup() { JSpinner resultsPerPage = new JSpinner( new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1)); resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue())); @@ -742,7 +635,7 @@ public class JadxSettingsWindow extends JDialog { })); fileExtField.setText(settings.getSrhResourceFileExt()); - SettingsGroupPanel searchGroup = new SettingsGroupPanel(NLS.str("preferences.search_group_title")); + SettingsGroup searchGroup = new SettingsGroup(NLS.str("preferences.search_group_title")); searchGroup.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage); searchGroup.addRow(NLS.str("preferences.res_skip_file"), sizeLimit); searchGroup.addRow(NLS.str("preferences.res_file_ext"), fileExtField); @@ -753,7 +646,7 @@ public class JadxSettingsWindow extends JDialog { settings.sync(); enableComponents(this, false); SwingUtilities.invokeLater(() -> { - if (needReload) { + if (shouldReload()) { mainWindow.reopen(); } if (!settings.getLangLocale().equals(prevLang)) { @@ -809,10 +702,20 @@ public class JadxSettingsWindow extends JDialog { NLS.str("preferences.copy_message")); } - private void needReload() { + void needReload() { needReload = true; } + private boolean shouldReload() { + return needReload || !startSettingsHash.equals(calcSettingsHash()); + } + + @SuppressWarnings("resource") + private String calcSettingsHash() { + JadxDecompiler decompiler = mainWindow.getWrapper().getCurrentDecompiler().orElse(null); + return settings.toJadxArgs().makeCodeArgsHash(decompiler); + } + @Override public void dispose() { settings.saveWindowPos(this); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/PluginsSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/PluginsSettings.java new file mode 100644 index 000000000..feaa3992b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/PluginsSettings.java @@ -0,0 +1,171 @@ +package jadx.gui.settings.ui; + +import java.awt.event.ItemEvent; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.IntSupplier; + +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.gui.ISettingsGroup; +import jadx.api.plugins.gui.JadxGuiContext; +import jadx.api.plugins.options.JadxPluginOptions; +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.gui.plugins.context.GuiPluginContext; +import jadx.gui.settings.JadxProject; +import jadx.gui.settings.JadxSettings; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.plugins.CollectPluginOptions; +import jadx.gui.utils.ui.DocumentUpdateListener; + +public class PluginsSettings { + private static final Logger LOG = LoggerFactory.getLogger(PluginsSettings.class); + + private final MainWindow mainWindow; + private final JadxSettings settings; + + public PluginsSettings(MainWindow mainWindow, JadxSettings settings) { + this.mainWindow = mainWindow; + this.settings = settings; + } + + public SettingsGroup build() { + SettingsGroup pluginsGroup = new SubSettingsGroup(NLS.str("preferences.plugins")); + fillMainSettings(pluginsGroup); + List list = new CollectPluginOptions(mainWindow).build(); + for (PluginContext context : list) { + ISettingsGroup pluginGroup = buildPluginGroup(context); + if (pluginGroup != null) { + pluginsGroup.getSubGroups().add(pluginGroup); + } + } + return pluginsGroup; + } + + private void fillMainSettings(SettingsGroup settingsGroup) { + JPanel panel = settingsGroup.getPanel(); + panel.add(new JPanel()); + } + + private ISettingsGroup buildPluginGroup(PluginContext context) { + JadxGuiContext guiContext = context.getGuiContext(); + if (guiContext instanceof GuiPluginContext) { + GuiPluginContext pluginGuiContext = ((GuiPluginContext) guiContext); + ISettingsGroup customSettingsGroup = pluginGuiContext.getCustomSettingsGroup(); + if (customSettingsGroup != null) { + return customSettingsGroup; + } + } + JadxPluginOptions options = context.getOptions(); + if (options == null) { + return null; + } + List optionsDescriptions = options.getOptionsDescriptions(); + if (optionsDescriptions.isEmpty()) { + return null; + } + SettingsGroup settingsGroup = new SettingsGroup(context.getPluginInfo().getName()); + addOptions(settingsGroup, optionsDescriptions); + return settingsGroup; + } + + public void addOptions(SettingsGroup pluginGroup, List optionsDescriptions) { + for (OptionDescription opt : optionsDescriptions) { + if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) { + continue; + } + String optName = opt.name(); + String title = opt.description(); + Consumer updateFunc; + String curValue; + if (opt.getFlags().contains(OptionFlag.PER_PROJECT)) { + JadxProject project = mainWindow.getProject(); + updateFunc = value -> project.updatePluginOptions(m -> m.put(optName, value)); + curValue = project.getPluginOption(optName); + } else { + Map optionsMap = settings.getPluginOptions(); + updateFunc = value -> optionsMap.put(optName, value); + curValue = optionsMap.get(optName); + } + String value = curValue != null ? curValue : opt.defaultValue(); + + JComponent editor = null; + if (opt.values().isEmpty() || opt.getType() == OptionType.BOOLEAN) { + try { + editor = getPluginOptionEditor(opt, value, updateFunc); + } catch (Exception e) { + LOG.error("Failed to add editor for plugin option: {}", optName, e); + } + } else { + JComboBox combo = new JComboBox<>(opt.values().toArray(new String[0])); + combo.setSelectedItem(value); + combo.addActionListener(e -> updateFunc.accept((String) combo.getSelectedItem())); + editor = combo; + } + if (editor != null) { + JLabel label = pluginGroup.addRow(title, editor); + boolean enabled = !opt.getFlags().contains(OptionFlag.DISABLE_IN_GUI); + if (!enabled) { + label.setEnabled(false); + editor.setEnabled(false); + } + } + } + } + + private JComponent getPluginOptionEditor(OptionDescription opt, String value, Consumer updateFunc) { + switch (opt.getType()) { + case STRING: + JTextField textField = new JTextField(); + textField.setText(value == null ? "" : value); + textField.getDocument().addDocumentListener( + new DocumentUpdateListener(event -> updateFunc.accept(textField.getText()))); + return textField; + + case NUMBER: + JSpinner numberField = new JSpinner(); + numberField.setValue(safeStringToInt(value, () -> safeStringToInt(opt.defaultValue(), () -> { + throw new IllegalArgumentException("Failed to parse integer default value: " + opt.defaultValue()); + }))); + numberField.addChangeListener(e -> updateFunc.accept(numberField.getValue().toString())); + return numberField; + + case BOOLEAN: + JCheckBox boolField = new JCheckBox(); + boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true")); + boolField.addItemListener(e -> { + boolean editorValue = e.getStateChange() == ItemEvent.SELECTED; + updateFunc.accept(editorValue ? "yes" : "no"); + }); + return boolField; + } + return null; + } + + private static int safeStringToInt(String value, IntSupplier defValueSupplier) { + if (value == null) { + return defValueSupplier.getAsInt(); + } + try { + return Integer.parseInt(value); + } catch (Exception e) { + LOG.warn("Failed parse string to int: {}", value, e); + return defValueSupplier.getAsInt(); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroupPanel.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroup.java similarity index 75% rename from jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroupPanel.java rename to jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroup.java index 12b1e660f..53d4ba97c 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroupPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroup.java @@ -11,17 +11,20 @@ import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; -public class SettingsGroupPanel extends JPanel { +import jadx.api.plugins.gui.ISettingsGroup; + +public class SettingsGroup implements ISettingsGroup { private static final long serialVersionUID = -6487309975896192544L; private final String title; + private final JPanel panel; private final GridBagConstraints c; private int row; - public SettingsGroupPanel(String title) { + public SettingsGroup(String title) { this.title = title; - setBorder(BorderFactory.createTitledBorder(title)); - setLayout(new GridBagLayout()); + panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createTitledBorder(title)); c = new GridBagConstraints(); c.insets = new Insets(5, 5, 5, 5); c.weighty = 1.0; @@ -41,7 +44,7 @@ public class SettingsGroupPanel extends JPanel { c.anchor = GridBagConstraints.LINE_START; c.weightx = 0.8; c.fill = GridBagConstraints.NONE; - add(jLabel, c); + panel.add(jLabel, c); c.gridx = 1; c.gridwidth = GridBagConstraints.REMAINDER; c.anchor = GridBagConstraints.CENTER; @@ -53,20 +56,30 @@ public class SettingsGroupPanel extends JPanel { comp.setToolTipText(tooltip); } - add(comp, c); + panel.add(comp, c); comp.addPropertyChangeListener("enabled", evt -> jLabel.setEnabled((boolean) evt.getNewValue())); return jLabel; } public void end() { - add(Box.createVerticalGlue()); + panel.add(Box.createVerticalGlue()); } + @Override + public JComponent buildComponent() { + return panel; + } + + @Override public String getTitle() { return title; } + public JPanel getPanel() { + return panel; + } + @Override public String toString() { return title; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTree.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTree.java index 238561daf..3f6cbc163 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTree.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTree.java @@ -3,6 +3,7 @@ package jadx.gui.settings.ui; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import javax.swing.JPanel; import javax.swing.JTree; @@ -15,43 +16,41 @@ import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; +import jadx.api.plugins.gui.ISettingsGroup; import jadx.gui.utils.NLS; public class SettingsTree extends JTree { - public void init(JPanel groupPanel, List groups) { + public void init(JPanel groupPanel, List groups) { DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode(NLS.str("preferences.title")); - for (SettingsGroupPanel group : groups) { - treeRoot.add(new DefaultMutableTreeNode(group)); - } + addGroups(treeRoot, groups); setModel(new DefaultTreeModel(treeRoot)); getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); setFocusable(false); - addTreeSelectionListener(e -> { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) getLastSelectedPathComponent(); - Object obj = node.getUserObject(); - groupPanel.removeAll(); - if (obj instanceof SettingsGroupPanel) { - SettingsGroupPanel panel = (SettingsGroupPanel) obj; - groupPanel.add(panel); - } - groupPanel.updateUI(); - }); + addTreeSelectionListener(e -> switchGroup(groupPanel)); // expand all nodes and disallow collapsing setNodeExpandedState(this, treeRoot, true); - addTreeWillExpandListener(new TreeWillExpandListener() { - @Override - public void treeWillExpand(TreeExpansionEvent event) { - } - - @Override - public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { - throw new ExpandVetoException(event, "Collapsing tree not allowed"); - } - }); + addTreeWillExpandListener(new DisableRootCollapseListener(treeRoot)); addSelectionRow(1); } + private static void addGroups(DefaultMutableTreeNode base, List groups) { + for (ISettingsGroup group : groups) { + SettingsTreeNode node = new SettingsTreeNode(group); + base.add(node); + addGroups(node, group.getSubGroups()); + } + } + + private void switchGroup(JPanel groupPanel) { + Object selected = getLastSelectedPathComponent(); + groupPanel.removeAll(); + if (selected instanceof SettingsTreeNode) { + groupPanel.add(((SettingsTreeNode) selected).getGroup().buildComponent()); + } + groupPanel.updateUI(); + } + private static void setNodeExpandedState(JTree tree, TreeNode node, boolean expanded) { ArrayList list = Collections.list(node.children()); for (TreeNode treeNode : list) { @@ -68,4 +67,24 @@ public class SettingsTree extends JTree { tree.collapsePath(path); } } + + private static class DisableRootCollapseListener implements TreeWillExpandListener { + private final DefaultMutableTreeNode treeRoot; + + public DisableRootCollapseListener(DefaultMutableTreeNode treeRoot) { + this.treeRoot = treeRoot; + } + + @Override + public void treeWillExpand(TreeExpansionEvent event) { + } + + @Override + public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { + Object current = event.getPath().getLastPathComponent(); + if (Objects.equals(current, treeRoot)) { + throw new ExpandVetoException(event, "Root collapsing not allowed"); + } + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTreeNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTreeNode.java new file mode 100644 index 000000000..5685aea3a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTreeNode.java @@ -0,0 +1,22 @@ +package jadx.gui.settings.ui; + +import javax.swing.tree.DefaultMutableTreeNode; + +import jadx.api.plugins.gui.ISettingsGroup; + +public class SettingsTreeNode extends DefaultMutableTreeNode { + private final ISettingsGroup group; + + public SettingsTreeNode(ISettingsGroup group) { + this.group = group; + } + + public ISettingsGroup getGroup() { + return group; + } + + @Override + public String toString() { + return group.getTitle(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/SubSettingsGroup.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/SubSettingsGroup.java new file mode 100644 index 000000000..bb508e9b6 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/SubSettingsGroup.java @@ -0,0 +1,20 @@ +package jadx.gui.settings.ui; + +import java.util.ArrayList; +import java.util.List; + +import jadx.api.plugins.gui.ISettingsGroup; + +public class SubSettingsGroup extends SettingsGroup { + + private final List groups = new ArrayList<>(); + + public SubSettingsGroup(String title) { + super(title); + } + + @Override + public List getSubGroups() { + return groups; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java index d295ca27c..8b88c1a5f 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java @@ -9,7 +9,9 @@ import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; -import jadx.gui.JadxWrapper; +import jadx.gui.plugins.context.CommonGuiPluginsContext; +import jadx.gui.plugins.context.GuiPluginContext; +import jadx.gui.ui.MainWindow; import jadx.plugins.tools.JadxExternalPluginsLoader; /** @@ -19,21 +21,26 @@ import jadx.plugins.tools.JadxExternalPluginsLoader; */ public class CollectPluginOptions { - private final JadxWrapper wrapper; + private final MainWindow mainWindow; - public CollectPluginOptions(JadxWrapper wrapper) { - this.wrapper = wrapper; + public CollectPluginOptions(MainWindow mainWindow) { + this.mainWindow = mainWindow; } public List build() { SortedSet allPlugins = new TreeSet<>(); - wrapper.getCurrentDecompiler() + mainWindow.getWrapper().getCurrentDecompiler() .ifPresent(decompiler -> allPlugins.addAll(decompiler.getPluginManager().getResolvedPluginContexts())); // collect and init not loaded plugins in new context try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) { JadxPluginManager pluginManager = decompiler.getPluginManager(); pluginManager.load(new JadxExternalPluginsLoader()); + CommonGuiPluginsContext guiPluginsContext = new CommonGuiPluginsContext(mainWindow); + decompiler.getPluginManager().registerAddPluginListener(pluginContext -> { + GuiPluginContext guiContext = guiPluginsContext.buildForPlugin(pluginContext); + pluginContext.setGuiContext(guiContext); + }); SortedSet missingPlugins = new TreeSet<>(); for (PluginContext context : pluginManager.getAllPluginContexts()) { if (!allPlugins.contains(context)) { diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java index 80a68005e..c968fc9e0 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java @@ -23,7 +23,7 @@ public class DexInputPlugin implements JadxPlugin { @Override public JadxPluginInfo getPluginInfo() { - return new JadxPluginInfo(PLUGIN_ID, "DexInput", "Load .dex and .apk files"); + return new JadxPluginInfo(PLUGIN_ID, "Dex Input", "Load .dex and .apk files"); } @Override diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 474c177d2..146fe6b70 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -23,7 +23,7 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput { public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo( PLUGIN_ID, - "JavaConvert", + "Java Convert", "Convert .class, .jar and .aar files to dex", "java-input"); } diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt index e12b322b9..0302bda0b 100644 --- a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt @@ -2,7 +2,7 @@ package jadx.plugins.kotlin.metadata import jadx.api.plugins.options.OptionDescription import jadx.api.plugins.options.impl.BaseOptionsParser -import jadx.api.plugins.options.impl.JadxOptionDescription +import jadx.api.plugins.options.impl.JadxOptionDescription.booleanOption import jadx.plugins.kotlin.metadata.KotlinMetadataPlugin.Companion.PLUGIN_ID class KotlinMetadataOptions : BaseOptionsParser() { @@ -33,27 +33,16 @@ class KotlinMetadataOptions : BaseOptionsParser() { override fun getOptionsDescriptions(): List { return listOf( - JadxOptionDescription.booleanOption(CLASS_ALIAS_OPT, "rename class alias", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(METHOD_ARGS_OPT, "rename function arguments", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(FIELDS_OPT, "rename fields", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(COMPANION_OPT, "rename companion object", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(DATA_CLASS_OPT, "add data class modifier", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(TO_STRING_OPT, "rename fields using toString", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), - JadxOptionDescription.booleanOption(GETTERS_OPT, "rename simple getters to field names", true) - .withFlag(OptionDescription.OptionFlag.PER_PROJECT), + booleanOption(CLASS_ALIAS_OPT, "rename class alias", true), + booleanOption(METHOD_ARGS_OPT, "rename function arguments", true), + booleanOption(FIELDS_OPT, "rename fields", true), + booleanOption(COMPANION_OPT, "rename companion object", true), + booleanOption(DATA_CLASS_OPT, "add data class modifier", true), + booleanOption(TO_STRING_OPT, "rename fields using toString", true), + booleanOption(GETTERS_OPT, "rename simple getters to field names", true), ) } - override fun toString(): String { - return "KotlinMetadataOptions(isClassAlias=$isClassAlias, isMethodArgs=$isMethodArgs, isFields=$isFields, isCompanion=$isCompanion, isDataClass=$isDataClass, isToString=$isToString, isGetters=$isGetters)" - } - companion object { const val CLASS_ALIAS_OPT = "$PLUGIN_ID.class-alias" const val METHOD_ARGS_OPT = "$PLUGIN_ID.method-args" diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java index 10cb386c8..662ac3218 100644 --- a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java @@ -10,7 +10,7 @@ import org.jetbrains.annotations.Nullable; import net.fabricmc.mappingio.format.MappingFormat; import jadx.api.plugins.options.OptionDescription; -import jadx.api.plugins.options.OptionDescription.OptionFlag; +import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.impl.BaseOptionsParser; import jadx.api.plugins.options.impl.JadxOptionDescription; diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptOptionsUI.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptOptionsUI.kt new file mode 100644 index 000000000..fe7aca9e9 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptOptionsUI.kt @@ -0,0 +1,30 @@ +package jadx.plugins.script + +import jadx.api.plugins.gui.ISettingsGroup +import jadx.api.plugins.gui.JadxGuiContext +import jadx.plugins.script.runtime.data.JadxScriptAllOptions +import javax.swing.JPanel + +object JadxScriptOptionsUI { + + fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) { + val settings = guiContext.settings() + val subGroups = scriptOptions.descriptions + .groupBy { it.script } + .map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) } + .toList() + settings.setCustomSettings(EmptyRootGroup("Scripts", subGroups)) + } +} + +private class EmptyRootGroup( + private val title: String, + private val subGroups: List, +) : ISettingsGroup { + + override fun getTitle() = title + + override fun buildComponent() = JPanel() + + override fun getSubGroups() = subGroups +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt index cd1a0178e..dd6a46708 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt @@ -16,6 +16,7 @@ class JadxScriptPlugin : JadxPlugin { val scripts = ScriptEval().process(init, scriptOptions) if (scripts.isNotEmpty()) { init.addPass(JadxScriptAfterLoadPass(scripts)) + init.guiContext?.let { JadxScriptOptionsUI.setup(it, scriptOptions) } } } } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt deleted file mode 100644 index 8b1378917..000000000 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt index 9f5fcff13..e41a39a32 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Options.kt @@ -2,21 +2,30 @@ package jadx.plugins.script.runtime.data import jadx.api.plugins.options.JadxPluginOptions import jadx.api.plugins.options.OptionDescription -import jadx.api.plugins.options.OptionDescription.OptionType +import jadx.api.plugins.options.OptionType import jadx.api.plugins.options.impl.JadxOptionDescription import jadx.plugins.script.runtime.JadxScriptInstance class JadxScriptAllOptions : JadxPluginOptions { lateinit var values: Map - val descriptions: MutableList = mutableListOf() + val descriptions: MutableList = mutableListOf() override fun setOptions(options: Map) { values = options } - override fun getOptionsDescriptions(): MutableList = descriptions + override fun getOptionsDescriptions(): List = descriptions } +class ScriptOptionDesc( + val script: String, + optName: String, + desc: String, + defaultValue: String?, + values: List, + type: OptionType, +) : JadxOptionDescription("jadx-script.$script.$optName", desc, defaultValue, values, type) + class ScriptOption( val name: String, val id: String, @@ -53,9 +62,10 @@ class JadxScriptOptions( type: OptionType = OptionType.STRING, convert: (String?) -> T, ): ScriptOption { - val id = "jadx-script.${jadx.scriptName}.$name" - options.descriptions.add(JadxOptionDescription(id, desc, defaultValue, values, type)) - return ScriptOption(name, id) { convert.invoke(options.values[id]) } + val optDesc = ScriptOptionDesc(jadx.scriptName, name, desc, defaultValue, values, type) + options.descriptions.add(optDesc) + val optId = optDesc.name() + return ScriptOption(name, optId) { convert.invoke(options.values[optId]) } } fun registerString(