feat(plugins): allow to set custom settings page in jadx-gui
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<String> supplier);
|
||||
|
||||
|
||||
@@ -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<ISettingsGroup> getSubGroups() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<OptionDescription> options);
|
||||
}
|
||||
@@ -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<OptionFlag> getFlags() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package jadx.api.plugins.options;
|
||||
|
||||
public enum OptionType {
|
||||
STRING,
|
||||
NUMBER,
|
||||
BOOLEAN
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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<PluginContext> resolvedPlugins = new TreeSet<>();
|
||||
private final Map<String, String> provideSuggestions = new TreeMap<>();
|
||||
|
||||
private @Nullable JadxGuiContext guiContext;
|
||||
private final List<Consumer<PluginContext>> 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<PluginContext> listener) {
|
||||
this.addPluginListeners.add(listener);
|
||||
// run for already added plugins
|
||||
getAllPluginContexts().forEach(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PluginContext> {
|
||||
private final JadxDecompiler decompiler;
|
||||
@@ -86,14 +90,28 @@ public class PluginContext implements JadxPluginContext, Comparable<PluginContex
|
||||
}
|
||||
|
||||
public String getInputsHash() {
|
||||
if (inputsHashSupplier != null) {
|
||||
try {
|
||||
return inputsHashSupplier.get();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to get inputs hash for plugin: " + getPluginId(), e);
|
||||
if (inputsHashSupplier == null) {
|
||||
return defaultOptionsHash();
|
||||
}
|
||||
try {
|
||||
return inputsHashSupplier.get();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to get inputs hash for plugin: " + getPluginId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String defaultOptionsHash() {
|
||||
if (options == null) {
|
||||
return "";
|
||||
}
|
||||
Map<String, String> 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+23
-40
@@ -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<PluginContext, GuiPluginContext> pluginsMap = new HashMap<>();
|
||||
|
||||
private final List<CodePopupAction> 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<CodePopupAction> 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<ICodeNodeRef, Boolean> enabled,
|
||||
@Nullable String keyBinding, Consumer<ICodeNodeRef> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ICodeNodeRef, Boolean> enabled,
|
||||
@Nullable String keyBinding, Consumer<ICodeNodeRef> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<OptionDescription> options) {
|
||||
MainWindow mainWindow = guiPluginContext.getCommonContext().getMainWindow();
|
||||
PluginsSettings pluginsSettings = new PluginsSettings(mainWindow, mainWindow.getSettings());
|
||||
SubSettingsGroup settingsGroup = new SubSettingsGroup(title);
|
||||
pluginsSettings.addOptions(settingsGroup, options);
|
||||
return settingsGroup;
|
||||
}
|
||||
}
|
||||
@@ -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<SettingsGroupPanel> groups = new ArrayList<>();
|
||||
List<ISettingsGroup> 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<LangLocale> 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<PluginContext> 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<String> 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<String, String> 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<String> 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<String> 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> 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);
|
||||
|
||||
@@ -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<PluginContext> 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<OptionDescription> 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<OptionDescription> optionsDescriptions) {
|
||||
for (OptionDescription opt : optionsDescriptions) {
|
||||
if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) {
|
||||
continue;
|
||||
}
|
||||
String optName = opt.name();
|
||||
String title = opt.description();
|
||||
Consumer<String> 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<String, String> 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<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-7
@@ -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;
|
||||
@@ -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<SettingsGroupPanel> groups) {
|
||||
public void init(JPanel groupPanel, List<ISettingsGroup> 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<ISettingsGroup> 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<? extends TreeNode> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<ISettingsGroup> groups = new ArrayList<>();
|
||||
|
||||
public SubSettingsGroup(String title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ISettingsGroup> getSubGroups() {
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
@@ -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<PluginContext> build() {
|
||||
SortedSet<PluginContext> 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<PluginContext> missingPlugins = new TreeSet<>();
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
if (!allPlugins.contains(context)) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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");
|
||||
}
|
||||
|
||||
+8
-19
@@ -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<OptionDescription> {
|
||||
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"
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+30
@@ -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>,
|
||||
) : ISettingsGroup {
|
||||
|
||||
override fun getTitle() = title
|
||||
|
||||
override fun buildComponent() = JPanel()
|
||||
|
||||
override fun getSubGroups() = subGroups
|
||||
}
|
||||
+1
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
|
||||
+16
-6
@@ -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<String, String>
|
||||
val descriptions: MutableList<OptionDescription> = mutableListOf()
|
||||
val descriptions: MutableList<ScriptOptionDesc> = mutableListOf()
|
||||
|
||||
override fun setOptions(options: Map<String, String>) {
|
||||
values = options
|
||||
}
|
||||
|
||||
override fun getOptionsDescriptions(): MutableList<OptionDescription> = descriptions
|
||||
override fun getOptionsDescriptions(): List<OptionDescription> = descriptions
|
||||
}
|
||||
|
||||
class ScriptOptionDesc(
|
||||
val script: String,
|
||||
optName: String,
|
||||
desc: String,
|
||||
defaultValue: String?,
|
||||
values: List<String>,
|
||||
type: OptionType,
|
||||
) : JadxOptionDescription("jadx-script.$script.$optName", desc, defaultValue, values, type)
|
||||
|
||||
class ScriptOption<T>(
|
||||
val name: String,
|
||||
val id: String,
|
||||
@@ -53,9 +62,10 @@ class JadxScriptOptions(
|
||||
type: OptionType = OptionType.STRING,
|
||||
convert: (String?) -> T,
|
||||
): ScriptOption<T> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user