feat(gui): support scripts in UI

This commit is contained in:
Skylot
2022-07-16 20:41:23 +01:00
parent e5e64365fc
commit 18fe9f305c
39 changed files with 752 additions and 49 deletions
@@ -28,6 +28,7 @@ import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.rename.RenameVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.plugins.context.PluginsContext;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
@@ -47,6 +48,7 @@ public class JadxWrapper {
private final MainWindow mainWindow;
private volatile @Nullable JadxDecompiler decompiler;
private PluginsContext pluginsContext;
public JadxWrapper(MainWindow mainWindow) {
this.mainWindow = mainWindow;
@@ -62,6 +64,8 @@ public class JadxWrapper {
jadxArgs.setCodeData(project.getCodeData());
this.decompiler = new JadxDecompiler(jadxArgs);
this.pluginsContext = new PluginsContext(mainWindow);
this.decompiler.setJadxGuiContext(pluginsContext);
this.decompiler.load();
initCodeCache();
}
@@ -87,6 +91,10 @@ public class JadxWrapper {
decompiler.close();
decompiler = null;
}
if (pluginsContext != null) {
pluginsContext.reset();
pluginsContext = null;
}
}
} catch (Exception e) {
LOG.error("Jadx decompiler close error", e);
@@ -0,0 +1,47 @@
package jadx.gui.plugins.context;
import javax.swing.JMenu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.gui.JadxGuiContext;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ActionHandler;
public class PluginsContext implements JadxGuiContext {
private static final Logger LOG = LoggerFactory.getLogger(PluginsContext.class);
private final MainWindow mainWindow;
public PluginsContext(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
public void reset() {
JMenu pluginsMenu = mainWindow.getPluginsMenu();
pluginsMenu.removeAll();
pluginsMenu.setVisible(false);
}
@Override
public void uiRun(Runnable runnable) {
UiUtils.uiRun(runnable);
}
@Override
public void addMenuAction(String name, Runnable action) {
ActionHandler item = new ActionHandler(ev -> {
try {
mainWindow.getBackgroundExecutor().execute(name, action);
} catch (Exception e) {
LOG.error("Error running action for menu item: {}", name, e);
}
});
item.setNameAndDesc(name);
JMenu pluginsMenu = mainWindow.getPluginsMenu();
pluginsMenu.add(item);
pluginsMenu.setVisible(true);
}
}
@@ -8,6 +8,7 @@ import jadx.api.JavaClass;
import jadx.gui.settings.data.TabViewState;
import jadx.gui.settings.data.ViewPoint;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.MainWindow;
@@ -52,9 +53,15 @@ public class TabStateViewAdapter {
return mw.getCacheObject().getNodeCache().makeFrom(javaClass);
}
break;
case "resource":
JResource tmpNode = new JResource(null, tvs.getTabPath(), JResource.JResType.FILE);
return mw.getTreeRoot().searchNode(tmpNode); // equals method in JResource check only name
case "script":
return mw.getTreeRoot()
.followStaticPath("JInputs", "JInputScripts")
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
}
return null;
}
@@ -70,6 +77,11 @@ public class TabStateViewAdapter {
tvs.setTabPath(node.getName());
return true;
}
if (node instanceof JInputScript) {
tvs.setType("script");
tvs.setTabPath(node.getName());
return true;
}
return false;
}
}
@@ -2,6 +2,7 @@ package jadx.gui.treemodel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
@@ -13,8 +14,10 @@ import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.NLS;
@@ -123,6 +126,11 @@ public class JClass extends JLoadableNode {
return SyntaxConstants.SYNTAX_STYLE_JAVA;
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return RenameDialog.buildRenamePopup(mainWindow, this);
}
@Override
public Icon getIcon() {
AccessInfo accessInfo = cls.getAccessInfo();
@@ -0,0 +1,35 @@
package jadx.gui.treemodel;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public abstract class JEditableNode extends JNode {
private volatile boolean changed = false;
private final List<Consumer<Boolean>> changeListeners = new ArrayList<>();
public abstract void save(String newContent);
@Override
public boolean isEditable() {
return true;
}
public boolean isChanged() {
return changed;
}
public void setChanged(boolean changed) {
if (this.changed != changed) {
this.changed = changed;
for (Consumer<Boolean> changeListener : changeListeners) {
changeListener.accept(changed);
}
}
}
public void addChangeListener(Consumer<Boolean> listener) {
changeListeners.add(listener);
}
}
@@ -4,6 +4,7 @@ import java.util.Comparator;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
@@ -12,6 +13,8 @@ import jadx.api.JavaField;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.utils.OverlayIcon;
import jadx.gui.utils.UiUtils;
@@ -54,6 +57,11 @@ public class JField extends JNode {
return !field.getFieldNode().contains(AFlag.DONT_RENAME);
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return RenameDialog.buildRenamePopup(mainWindow, this);
}
@Override
public Icon getIcon() {
AccessInfo af = field.getAccessFlags();
@@ -0,0 +1,43 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import javax.swing.Icon;
import javax.swing.JPopupMenu;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.ui.SimpleMenuItem;
public class JInputFile extends JNode {
private final Path filePath;
public JInputFile(Path filePath) {
this.filePath = filePath;
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_files"), mainWindow::addFiles));
menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(filePath)));
return menu;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return Icons.FILE;
}
@Override
public String makeString() {
return filePath.getFileName().toString();
}
}
@@ -0,0 +1,45 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
public class JInputFiles extends JNode {
private static final ImageIcon INPUT_FILES_ICON = UiUtils.openSvgIcon("nodes/moduleDirectory");
public JInputFiles(List<Path> files) {
for (Path file : files) {
add(new JInputFile(file));
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_files"), mainWindow::addFiles));
return menu;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return INPUT_FILES_ICON;
}
@Override
public String makeString() {
return NLS.str("tree.input_files");
}
}
@@ -0,0 +1,101 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
public class JInputScript extends JEditableNode {
private static final Logger LOG = LoggerFactory.getLogger(JInputScript.class);
private static final ImageIcon SCRIPT_ICON = UiUtils.openSvgIcon("nodes/kotlin_script");
private final Path scriptPath;
private final String name;
public JInputScript(Path scriptPath) {
this.scriptPath = scriptPath;
this.name = scriptPath.getFileName().toString().replace(".jadx.kts", "");
}
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new CodeContentPanel(tabbedPane, this);
}
@Override
public @NotNull ICodeInfo getCodeInfo() {
try {
return new SimpleCodeInfo(FileUtils.readFile(scriptPath));
} catch (Exception e) {
throw new JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e);
}
}
@Override
public void save(String newContent) {
try {
FileUtils.writeFile(scriptPath, newContent);
LOG.debug("Script saved: {}", scriptPath.toAbsolutePath());
} catch (Exception e) {
throw new JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e);
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(scriptPath)));
return menu;
}
@Override
public boolean isEditable() {
return true;
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_KOTLIN;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return SCRIPT_ICON;
}
@Override
public String getName() {
return name;
}
@Override
public String makeString() {
return name;
}
}
@@ -0,0 +1,46 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
public class JInputScripts extends JNode {
private static final ImageIcon INPUT_SCRIPTS_ICON = UiUtils.openSvgIcon("nodes/scriptsModel");
public JInputScripts(List<Path> scripts) {
for (Path script : scripts) {
add(new JInputScript(script));
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
return menu;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return INPUT_SCRIPTS_ICON;
}
@Override
public String makeString() {
return NLS.str("tree.input_scripts");
}
}
@@ -0,0 +1,50 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class JInputs extends JNode {
private static final ImageIcon INPUTS_ICON = UiUtils.openSvgIcon("nodes/projectStructure");
public JInputs(JadxWrapper wrapper) {
List<Path> inputs = wrapper.getProject().getFilePaths();
List<Path> files = FileUtils.expandDirs(inputs);
List<Path> scripts = new ArrayList<>();
Iterator<Path> it = files.iterator();
while (it.hasNext()) {
Path file = it.next();
if (file.getFileName().toString().endsWith(".jadx.kts")) {
scripts.add(file);
it.remove();
}
}
add(new JInputFiles(files));
add(new JInputScripts(scripts));
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return INPUTS_ICON;
}
@Override
public String makeString() {
return NLS.str("tree.inputs_title");
}
}
@@ -5,6 +5,7 @@ import java.util.Iterator;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
@@ -14,6 +15,8 @@ import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.utils.Icons;
import jadx.gui.utils.OverlayIcon;
import jadx.gui.utils.UiUtils;
@@ -107,6 +110,11 @@ public class JMethod extends JNode {
return !mth.getMethodNode().contains(AFlag.DONT_RENAME);
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return RenameDialog.buildRenamePopup(mainWindow, this);
}
String makeBaseString() {
if (mth.isClassInit()) {
return "{...}";
@@ -1,8 +1,11 @@
package jadx.gui.treemodel;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.function.Predicate;
import javax.swing.Icon;
import javax.swing.JPopupMenu;
import javax.swing.tree.DefaultMutableTreeNode;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
@@ -11,6 +14,7 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.JavaNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
@@ -45,6 +49,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return ICodeInfo.EMPTY;
}
public boolean isEditable() {
return false;
}
public abstract Icon getIcon();
public String getName() {
@@ -59,6 +67,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return false;
}
public @Nullable JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return null;
}
public abstract String makeString();
public String makeStringHtml() {
@@ -97,6 +109,17 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return makeLongStringHtml();
}
public @Nullable JNode searchNode(Predicate<JNode> filter) {
Enumeration<?> en = this.breadthFirstEnumeration();
while (en.hasMoreElements()) {
JNode node = (JNode) en.nextElement();
if (filter.test(node)) {
return node;
}
}
return null;
}
private static final Comparator<JNode> COMPARATOR = Comparator
.comparing(JNode::makeLongString)
.thenComparingInt(JNode::getPos);
@@ -6,10 +6,13 @@ import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import jadx.api.JavaPackage;
import jadx.core.utils.Utils;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.popupmenu.JPackagePopupMenu;
import jadx.gui.utils.UiUtils;
public class JPackage extends JNode {
@@ -68,6 +71,11 @@ public class JPackage extends JNode {
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return new JPackagePopupMenu(mainWindow, this);
}
@Override
public String getName() {
return name;
@@ -24,6 +24,7 @@ import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.res.ResTableHelper;
@@ -32,8 +33,6 @@ public class JResource extends JLoadableNode {
private static final long serialVersionUID = -201018424302612434L;
private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/resourcesRoot");
private static final ImageIcon FOLDER_ICON = UiUtils.openSvgIcon("nodes/folder");
private static final ImageIcon FILE_ICON = UiUtils.openSvgIcon("nodes/file_any_type");
private static final ImageIcon ARSC_ICON = UiUtils.openSvgIcon("nodes/resourceBundle");
private static final ImageIcon XML_ICON = UiUtils.openSvgIcon("nodes/xml");
private static final ImageIcon IMAGE_ICON = UiUtils.openSvgIcon("nodes/ImagesFileType");
@@ -244,7 +243,7 @@ public class JResource extends JLoadableNode {
case ROOT:
return ROOT_ICON;
case DIR:
return FOLDER_ICON;
return Icons.FOLDER;
case FILE:
ResourceType resType = resFile.getType();
@@ -266,7 +265,7 @@ public class JResource extends JLoadableNode {
}
return UNKNOWN_ICON;
}
return FILE_ICON;
return Icons.FILE;
}
public static boolean isSupportedForView(ResourceType type) {
@@ -3,17 +3,21 @@ package jadx.gui.treemodel;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.tree.TreeNode;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JResource.JResType;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -35,6 +39,7 @@ public class JRoot extends JNode {
public final void update() {
removeAllChildren();
add(new JInputs(wrapper));
add(new JSources(this, wrapper));
List<ResourceFile> resources = wrapper.getResources();
@@ -87,7 +92,7 @@ public class JRoot extends JNode {
return null;
}
public JNode searchNode(JNode node) {
public @Nullable JNode searchNode(JNode node) {
Enumeration<?> en = this.breadthFirstEnumeration();
while (en.hasMoreElements()) {
Object obj = en.nextElement();
@@ -98,6 +103,30 @@ public class JRoot extends JNode {
return null;
}
public JNode followStaticPath(String... path) {
List<String> list = Arrays.asList(path);
JNode node = getNodeByClsPath(this, 0, list);
if (node == null) {
throw new JadxRuntimeException("Incorrect static path in tree: " + list);
}
return node;
}
private static @Nullable JNode getNodeByClsPath(JNode start, int pos, List<String> path) {
if (pos >= path.size()) {
return start;
}
String clsName = path.get(pos);
Enumeration<TreeNode> en = start.children();
while (en.hasMoreElements()) {
JNode node = (JNode) en.nextElement();
if (node.getClass().getSimpleName().equals(clsName)) {
return getNodeByClsPath(node, pos + 1, path);
}
}
return null;
}
public boolean isFlatPackages() {
return flatPackages;
}
@@ -134,7 +163,11 @@ public class JRoot extends JNode {
@Override
public String makeString() {
List<Path> paths = wrapper.getProject().getFilePaths();
JadxProject project = wrapper.getProject();
if (project.getProjectPath() != null) {
return project.getName();
}
List<Path> paths = project.getFilePaths();
int count = paths.size();
if (count == 0) {
return "File not open";
@@ -87,6 +87,7 @@ import jadx.api.JavaNode;
import jadx.api.ResourceFile;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.core.Jadx;
import jadx.core.export.TemplateFile;
import jadx.core.utils.ListUtils;
import jadx.core.utils.StringUtils;
import jadx.core.utils.files.FileUtils;
@@ -103,9 +104,7 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsWindow;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JLoadableNode;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JPackage;
import jadx.gui.treemodel.JResource;
@@ -117,7 +116,6 @@ import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.ui.dialog.ADBDialog;
import jadx.gui.ui.dialog.AboutDialog;
import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
@@ -125,7 +123,6 @@ import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.IssuesPanel;
import jadx.gui.ui.panel.JDebuggerPanel;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.ui.popupmenu.JPackagePopupMenu;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.update.JadxUpdate;
@@ -215,9 +212,11 @@ public class MainWindow extends JFrame {
private JDebuggerPanel debuggerPanel;
private JSplitPane verticalSplitter;
private List<ILoadListener> loadListeners = new ArrayList<>();
private final List<ILoadListener> loadListeners = new ArrayList<>();
private boolean loaded;
private JMenu pluginsMenu;
public MainWindow(JadxSettings settings) {
this.settings = settings;
this.cacheObject = new CacheObject();
@@ -403,6 +402,40 @@ public class MainWindow extends JFrame {
s -> update());
}
public void addNewScript() {
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.CUSTOM_SAVE);
fileDialog.setTitle(NLS.str("file.save"));
Path workingDir = project.getWorkingDir();
Path baseDir = workingDir != null ? workingDir : settings.getLastSaveFilePath();
fileDialog.setSelectedFile(baseDir.resolve("script.jadx.kts"));
fileDialog.setFileExtList(Collections.singletonList("jadx.kts"));
fileDialog.setSelectionMode(JFileChooser.FILES_ONLY);
List<Path> paths = fileDialog.show();
if (paths.size() != 1) {
return;
}
Path scriptFile = paths.get(0);
try {
TemplateFile tmpl = TemplateFile.fromResources("/files/script.jadx.kts.tmpl");
FileUtils.writeFile(scriptFile, tmpl.build());
} catch (Exception e) {
LOG.error("Failed to save new script file: {}", scriptFile, e);
}
List<Path> inputs = project.getFilePaths();
inputs.add(scriptFile);
project.setFilePaths(inputs);
project.save();
reopen();
}
public void removeInput(Path file) {
List<Path> inputs = project.getFilePaths();
inputs.remove(file);
project.setFilePaths(inputs);
project.save();
reopen();
}
public void open(Path path) {
open(Collections.singletonList(path), EMPTY_RUNNABLE);
}
@@ -746,15 +779,12 @@ public class MainWindow extends JFrame {
}
private void treeRightClickAction(MouseEvent e) {
JNode obj = getJNodeUnderMouse(e);
if (obj instanceof JPackage) {
JPackagePopupMenu menu = new JPackagePopupMenu(this, (JPackage) obj);
menu.show(e.getComponent(), e.getX(), e.getY());
} else if (obj instanceof JClass || obj instanceof JField || obj instanceof JMethod) {
JMenuItem jmi = new JMenuItem(NLS.str("popup.rename"));
jmi.addActionListener(action -> RenameDialog.rename(this, obj));
JPopupMenu menu = new JPopupMenu();
menu.add(jmi);
JNode node = getJNodeUnderMouse(e);
if (node == null) {
return;
}
JPopupMenu menu = node.onTreePopupMenu(this);
if (menu != null) {
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
@@ -903,7 +933,7 @@ public class MainWindow extends JFrame {
}
};
saveAllAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_all"));
saveAllAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_S, UiUtils.ctrlButton()));
saveAllAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton()));
Action exportAction = new AbstractAction(NLS.str("file.export_gradle"), ICON_EXPORT) {
@Override
@@ -912,7 +942,7 @@ public class MainWindow extends JFrame {
}
};
exportAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.export_gradle"));
exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton()));
exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));
JMenu recentProjects = new JMenu(NLS.str("menu.recent_projects"));
recentProjects.addMenuListener(new RecentProjectsMenuListener(recentProjects));
@@ -1117,6 +1147,10 @@ public class MainWindow extends JFrame {
nav.add(backAction);
nav.add(forwardAction);
pluginsMenu = new JMenu(NLS.str("menu.plugins"));
pluginsMenu.setMnemonic(KeyEvent.VK_P);
pluginsMenu.setVisible(false);
JMenu tools = new JMenu(NLS.str("menu.tools"));
tools.setMnemonic(KeyEvent.VK_T);
tools.add(decompileAllAction);
@@ -1142,6 +1176,7 @@ public class MainWindow extends JFrame {
menuBar.add(view);
menuBar.add(nav);
menuBar.add(tools);
menuBar.add(pluginsMenu);
menuBar.add(help);
setJMenuBar(menuBar);
@@ -1614,4 +1649,8 @@ public class MainWindow extends JFrame {
public void menuCanceled(MenuEvent e) {
}
}
public JMenu getPluginsMenu() {
return pluginsMenu;
}
}
@@ -18,6 +18,7 @@ import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicButtonUI;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JEditableNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.Icons;
@@ -53,13 +54,7 @@ public class TabComponent extends JPanel {
setOpaque(false);
JNode node = contentPanel.getNode();
String tabTitle;
if (node.getRootClass() != null) {
tabTitle = node.getRootClass().getName();
} else {
tabTitle = node.makeLongStringHtml();
}
label = new NodeLabel(tabTitle, node.disableHtml());
label = new NodeLabel(buildTabTitle(node), node.disableHtml());
label.setFont(getLabelFont());
String toolTip = contentPanel.getTabTooltip();
if (toolTip != null) {
@@ -67,6 +62,9 @@ public class TabComponent extends JPanel {
}
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
label.setIcon(node.getIcon());
if (node instanceof JEditableNode) {
((JEditableNode) node).addChangeListener(c -> label.setText(buildTabTitle(node)));
}
final JButton closeBtn = new JButton();
closeBtn.setIcon(Icons.CLOSE_INACTIVE);
@@ -104,6 +102,21 @@ public class TabComponent extends JPanel {
setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
}
private String buildTabTitle(JNode node) {
String tabTitle;
if (node.getRootClass() != null) {
tabTitle = node.getRootClass().getName();
} else {
tabTitle = node.makeLongStringHtml();
}
if (node instanceof JEditableNode) {
if (((JEditableNode) node).isChanged()) {
return "*" + tabTitle;
}
}
return tabTitle;
}
private JPopupMenu createTabPopupMenu(final ContentPanel contentPanel) {
JPopupMenu menu = new JPopupMenu();
@@ -46,6 +46,7 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JEditableNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ContentPanel;
@@ -53,6 +54,7 @@ import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.DocumentUpdateListener;
import jadx.gui.utils.ui.ZoomActions;
public abstract class AbstractCodeArea extends RSyntaxTextArea {
@@ -75,12 +77,14 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
protected ContentPanel contentPanel;
protected JNode node;
protected volatile boolean loaded = false;
public AbstractCodeArea(ContentPanel contentPanel, JNode node) {
this.contentPanel = contentPanel;
this.node = Objects.requireNonNull(node);
setMarkOccurrences(false);
setEditable(false);
setEditable(node.isEditable());
setCodeFoldingEnabled(false);
setFadeCurrentLineHighlight(true);
setCloseCurlyBraces(true);
@@ -91,10 +95,16 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
setLineWrap(settings.isCodeAreaLineWrap());
addWrapLineMenuAction(settings);
addCaretActions();
addFastCopyAction();
ZoomActions.register(this, settings, this::loadSettings);
if (node instanceof JEditableNode) {
JEditableNode editableNode = (JEditableNode) node;
addSaveActions(editableNode);
addChangeUpdates(editableNode);
} else {
addCaretActions();
addFastCopyAction();
}
}
private void addWrapLineMenuAction(JadxSettings settings) {
@@ -186,6 +196,26 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
});
}
private void addSaveActions(JEditableNode node) {
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_S && UiUtils.isCtrlDown(e)) {
node.save(AbstractCodeArea.this.getText());
node.setChanged(false);
}
}
});
}
private void addChangeUpdates(JEditableNode editableNode) {
getDocument().addDocumentListener(new DocumentUpdateListener(ev -> {
if (loaded) {
editableNode.setChanged(true);
}
}));
}
private String highlightCaretWord(String lastText, int pos) {
String text = getWordByPosition(pos);
if (StringUtils.isEmpty(text)) {
@@ -244,9 +274,14 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
/**
* Implement in this method the code that loads and sets the content to be displayed
* Call `setLoaded()` on load finish.
*/
public abstract void load();
public void setLoaded() {
this.loaded = true;
}
/**
* Implement in this method the code that reloads node from cache and sets the new content to be
* displayed
@@ -99,6 +99,7 @@ public final class CodeArea extends AbstractCodeArea {
if (getText().isEmpty()) {
setText(getCodeInfo().getCodeStr());
setCaretPosition(0);
setLoaded();
}
}
@@ -3,6 +3,9 @@ package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Point;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.IViewStateSupport;
@@ -10,6 +13,8 @@ import jadx.gui.ui.panel.IViewStateSupport;
public final class CodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport {
private static final long serialVersionUID = 5310536092010045565L;
private static final Logger LOG = LoggerFactory.getLogger(CodeContentPanel.class);
private final CodePanel codePanel;
public CodeContentPanel(TabbedPane panel, JNode jnode) {
@@ -59,8 +64,12 @@ public final class CodeContentPanel extends AbstractCodeContentPanel implements
@Override
public void restoreEditorViewState(EditorViewState viewState) {
codePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
codePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
try {
codePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
codePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
} catch (Exception e) {
LOG.error("Failed to restore view state", e);
}
}
@Override
@@ -93,6 +93,7 @@ public final class SmaliArea extends AbstractCodeArea {
curVersion = shouldUseSmaliPrinterV2();
model.load();
setCaretPosition(0);
setLoaded();
}
}
@@ -20,7 +20,9 @@ import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
@@ -84,6 +86,14 @@ public class RenameDialog extends JDialog {
return true;
}
public static JPopupMenu buildRenamePopup(MainWindow mainWindow, JNode node) {
JMenuItem jmi = new JMenuItem(NLS.str("popup.rename"));
jmi.addActionListener(action -> RenameDialog.rename(mainWindow, node));
JPopupMenu menu = new JPopupMenu();
menu.add(jmi);
return menu;
}
private RenameDialog(MainWindow mainWindow, JNode source, JNode node) {
super(mainWindow);
this.mainWindow = mainWindow;
@@ -16,6 +16,9 @@ import jadx.gui.utils.NLS;
public class FileDialogWrapper {
private static final List<String> OPEN_FILES_EXTS = Arrays.asList(
"apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts");
private final MainWindow mainWindow;
private boolean isOpen;
@@ -60,21 +63,27 @@ public class FileDialogWrapper {
private void initForMode(FileOpenMode mode) {
switch (mode) {
case OPEN:
case OPEN_PROJECT:
title = NLS.str("file.open_title");
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
currentDir = mainWindow.getSettings().getLastOpenFilePath();
isOpen = true;
break;
case OPEN:
title = NLS.str("file.open_title");
fileExtList = new ArrayList<>(OPEN_FILES_EXTS);
fileExtList.add(JadxProject.PROJECT_EXTENSION);
fileExtList.add("aab");
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
currentDir = mainWindow.getSettings().getLastOpenFilePath();
isOpen = true;
break;
case ADD:
if (mode == FileOpenMode.OPEN_PROJECT) {
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
title = NLS.str("file.open_title");
} else {
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
if (mode == FileOpenMode.OPEN) {
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
title = NLS.str("file.open_title");
} else {
title = NLS.str("file.add_files_action");
}
}
title = NLS.str("file.add_files_action");
fileExtList = OPEN_FILES_EXTS;
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
currentDir = mainWindow.getSettings().getLastOpenFilePath();
isOpen = true;
@@ -17,4 +17,7 @@ public class Icons {
public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark");
public static final ImageIcon START_PAGE = openSvgIcon("nodes/newWindow");
public static final ImageIcon FOLDER = UiUtils.openSvgIcon("nodes/folder");
public static final ImageIcon FILE = UiUtils.openSvgIcon("nodes/file_any_type");
}
@@ -25,6 +25,6 @@ public class DocumentUpdateListener implements DocumentListener {
@Override
public void changedUpdate(DocumentEvent event) {
this.listener.accept(event);
// ignore attributes change
}
}
@@ -0,0 +1,11 @@
package jadx.gui.utils.ui;
import javax.swing.JMenuItem;
public class SimpleMenuItem extends JMenuItem {
public SimpleMenuItem(String text, Runnable action) {
super(text);
addActionListener(ev -> action.run());
}
}
@@ -0,0 +1,5 @@
val jadx = getJadxInstance()
jadx.afterLoad {
log.info { "Hello from jadx script!" }
}
@@ -14,6 +14,7 @@ menu.text_search=Textsuche
menu.class_search=Klassen-Suche
menu.comment_search=Kommentar suchen
menu.tools=Tools
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
menu.deobfuscation=Deobfuskierung
menu.log=Log-Anzeige
@@ -33,6 +34,7 @@ file.live_reload=Live nachladen
file.live_reload_desc=Dateien bei Änderungen autom. neuladen
file.export_mappings_as=Zuordnungen exportieren als…
file.save_all=Alles speichern
#file.save=Save
file.export_gradle=Als Gradle-Projekt speichern
file.save_all_msg=Verzeichnis für das Speichern dekompilierter Ressourcen auswählen
file.exit=Beenden
@@ -41,6 +43,9 @@ file.exit=Beenden
#start_page.start=Start
#start_page.recent=Recent projects
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=Quelltexte
tree.resources_title=Ressourcen
tree.loading=Laden…
@@ -234,6 +239,10 @@ popup.search_comment=Kommentar suchen
popup.rename=Umbennen
popup.search=Suche "%s"
popup.search_global=Globale Suche "%s"
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
exclude_dialog.title=Paketauswahl
exclude_dialog.ok=OK
@@ -14,6 +14,7 @@ menu.text_search=Text search
menu.class_search=Class search
menu.comment_search=Comment searchF
menu.tools=Tools
menu.plugins=Plugins
menu.decompile_all=Decompile all classes
menu.deobfuscation=Deobfuscation
menu.log=Log Viewer
@@ -33,6 +34,7 @@ file.live_reload=Live reload
file.live_reload_desc=Auto reload files on changes
file.export_mappings_as=Export mappings as...
file.save_all=Save all
file.save=Save
file.export_gradle=Save as gradle project
file.save_all_msg=Select directory for save decompiled sources
file.exit=Exit
@@ -41,6 +43,9 @@ start_page.title=Start page
start_page.start=Start
start_page.recent=Recent projects
tree.inputs_title=Inputs
tree.input_files=Files
tree.input_scripts=Scripts
tree.sources_title=Source code
tree.resources_title=Resources
tree.loading=Loading...
@@ -234,6 +239,10 @@ popup.search_comment=Search comments
popup.rename=Rename
popup.search=Search "%s"
popup.search_global=Global Search "%s"
popup.remove=Remove
popup.add_files=Add files
popup.add_scripts=Add scripts
popup.new_script=New script
exclude_dialog.title=Package Selector
exclude_dialog.ok=OK
@@ -14,6 +14,7 @@ menu.text_search=Buscar texto
menu.class_search=Buscar clase
#menu.comment_search=Comment search
menu.tools=Herramientas
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
menu.deobfuscation=Desofuscación
menu.log=Visor log
@@ -33,6 +34,7 @@ file.open_title=Abrir archivo
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=Guardar todo
#file.save=Save
file.export_gradle=Guardar como proyecto Gradle
file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas
file.exit=Salir
@@ -41,6 +43,9 @@ file.exit=Salir
#start_page.start=Start
#start_page.recent=Recent projects
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=Código fuente
tree.resources_title=Recursos
tree.loading=Cargando...
@@ -234,6 +239,10 @@ popup.xposed=Copiar como fragmento de xposed
popup.rename=Nimeta ümber
#popup.search=
#popup.search_global=
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
#exclude_dialog.title=Package Selector
#exclude_dialog.ok=OK
@@ -14,6 +14,7 @@ menu.text_search=텍스트 검색
menu.class_search=클래스 검색
menu.comment_search=주석 검색
menu.tools=도구
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
menu.deobfuscation=난독화 해제
menu.log=로그 뷰어
@@ -33,6 +34,7 @@ file.live_reload=라이브 로드
file.live_reload_desc=파일 내용 변경 시 자동으로 다시 로드
file.export_mappings_as=다른 이름으로 매핑 내보내기...
file.save_all=모두 저장
#file.save=Save
file.export_gradle=Gradle 프로젝트로 저장
file.save_all_msg=디컴파일된 소스를 저장할 디렉토리 선택
file.exit=나가기
@@ -41,6 +43,9 @@ start_page.title=페이지 시작
start_page.start=시작
start_page.recent=최근 프로젝트
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=소스코드
tree.resources_title=리소스
tree.loading=로딩중...
@@ -234,6 +239,10 @@ popup.search_comment=주석 검색
popup.rename=이름 바꾸기
popup.search="%s" 검색
popup.search_global="%s" 전역 검색
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
exclude_dialog.title=패키지 선택기
exclude_dialog.ok=확인
@@ -14,6 +14,7 @@ menu.text_search=Buscar por texto
menu.class_search=Buscar por classe
menu.comment_search=Busca por comentário
menu.tools=Ferramentas
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
menu.deobfuscation=Desofuscar
menu.log=Visualizador de log
@@ -33,6 +34,7 @@ file.live_reload=Recarregar em tempo real
file.live_reload_desc=Recarregar arquivos automaticamente ao serem alterados
file.export_mappings_as=Exportar mappings como...
file.save_all=Salvar tudo
#file.save=Save
file.export_gradle=Salvar como um projeto gradle
file.save_all_msg=Selecionar diretório para salvar arquivos descompilados
file.exit=Sair
@@ -41,6 +43,9 @@ start_page.title=Página inicial
start_page.start=Começar
start_page.recent=Projetos recentes
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=Código fonte
tree.resources_title=Recursos
tree.loading=Carregando...
@@ -234,6 +239,10 @@ popup.search_comment=Buscar comentários
popup.rename=Renomear
popup.search=Buscar "%s"
popup.search_global=Busca global "%s"
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
exclude_dialog.title=Selecionar pacote
exclude_dialog.ok=OK
@@ -14,6 +14,7 @@ menu.text_search=文本搜索
menu.class_search=类名搜索
menu.comment_search=注释搜索
menu.tools=工具
#menu.plugins=Plugins
menu.decompile_all=反编译所有类
menu.deobfuscation=反混淆
menu.log=日志查看器
@@ -33,6 +34,7 @@ file.live_reload=实时重加载
file.live_reload_desc=文件变动时自动重载
file.export_mappings_as=导出映射为…
file.save_all=全部保存
#file.save=Save
file.export_gradle=另存为 Gradle 项目
file.save_all_msg=请选择保存反编译资源的目录
file.exit=退出
@@ -41,6 +43,9 @@ start_page.title=开始页面
start_page.start=开始
start_page.recent=最近项目
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=源代码
tree.resources_title=资源文件
tree.loading=加载中…
@@ -234,6 +239,10 @@ popup.search_comment=搜索注释
popup.rename=重命名
popup.search=搜索 “%s”
popup.search_global=全局搜索 “%s”
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
exclude_dialog.title=选择要排除的包
exclude_dialog.ok=确定
@@ -14,6 +14,7 @@ menu.text_search=文字搜尋
menu.class_search=類別搜尋
menu.comment_search=註解搜尋
menu.tools=工具
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
menu.deobfuscation=去模糊化
menu.log=日誌檢視器
@@ -33,6 +34,7 @@ file.live_reload=實時重新載入
file.live_reload_desc=更動後自動重新載入檔案
file.export_mappings_as=匯出對應為...
file.save_all=全部儲存
#file.save=Save
file.export_gradle=另存為 gradle 專案
file.save_all_msg=選擇儲存反編譯原始碼的路徑
file.exit=離開
@@ -41,6 +43,9 @@ start_page.title=開始頁面
start_page.start=開始
start_page.recent=近期專案
#tree.inputs_title=Inputs
#tree.input_files=Files
#tree.input_scripts=Scripts
tree.sources_title=原始碼
tree.resources_title=資源
tree.loading=載入中...
@@ -234,6 +239,10 @@ popup.search_comment=搜尋註解
popup.rename=重新命名
popup.search=搜尋 "%s"
popup.search_global=全域搜尋 "%s"
#popup.remove=Remove
#popup.add_files=Add files
#popup.add_scripts=Add scripts
#popup.new_script=New script
exclude_dialog.title=套件選擇
exclude_dialog.ok=OK
@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="kotlin_script">
<path id="Combined Shape" fill-rule="evenodd" clip-rule="evenodd" d="M13 1H3C3 1 4 2.5 4 4.5C4 5.5 3.5 6.75 3 8C2.5 9.25 2 10.5 2 11.5C2 13.5 3 15 3 15H8V8H13C13.5 6.75 14 5.5 14 4.5C14 2.5 13 1 13 1Z" fill="#9AA7B0" fill-opacity="0.8"/>
<path id="Vector" d="M16 16H9V9H16L12.5 12.5L16 16Z" fill="#B99BF8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<polygon fill="#389FD6" points="10 15 15 15 15 10 10 10"/>
<path fill="#6E6E6E" d="M14,9 L9,9 L9,13 L2,13 L2,5 L2,3 L6.60006714,3 L7.75640322,5 L14,5 L14,9 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

@@ -0,0 +1,9 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd" transform="translate(1 3)">
<path fill="#6E6E6E" d="M14,8 L14,10 L10,10 L0,10 L0,2 L0,0 L5.39876049,0 L6.7558671,2 L14,2 L14,10 L10,10 L6,10 L6,8 L10,8 L10,4 L14,4 L14,8 Z"/>
<rect width="3" height="3" x="11" y="5" fill="#389FD6"/>
<rect width="3" height="3" x="11" y="9" fill="#389FD6"/>
<rect width="3" height="3" x="7" y="9" fill="#389FD6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 661 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<path fill="#9AA7B0" d="M10,16 L16,16 L16,9 L10,9 L10,16 Z M11,15 L15.001,15 L15.001,10 L11,10 L11,15 Z"/>
<polygon fill="#9AA7B0" points="12 12.001 14 12.001 14 11 12 11"/>
<polygon fill="#9AA7B0" points="12 14.001 14 14.001 14 13 12 13"/>
<path fill="#6E6E6E" fill-opacity=".8" d="M7.9846,4 L6.6966,2.711 C6.3046,2.32 5.5326,2 4.9786,2 L1.0506,2 C1.0226,2 1.0006,2.023 1.0006,2.051 L1.0006,13 L9,13 L9,7.99092055 L15.0006,7.99092055 L15.0006,4 L7.9846,4 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 613 B