feat(gui): add option to set cache location, view/delete exists caches (#1941)

This commit is contained in:
Skylot
2023-07-16 22:10:32 +01:00
parent de603ef909
commit 5b7ebec7e3
29 changed files with 1109 additions and 74 deletions
@@ -14,7 +14,7 @@ import jadx.core.deobf.TldHelper;
public class BetterName {
private static final Logger LOG = LoggerFactory.getLogger(BetterName.class);
private static final boolean DEBUG = true;
private static final boolean DEBUG = false;
public static String compareAndGet(String first, String second) {
if (Objects.equals(first, second)) {
+1
View File
@@ -40,6 +40,7 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("org.apache.commons:commons-text:1.10.0")
implementation("commons-io:commons-io:2.13.0")
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
implementation("com.github.akarnokd:rxjava2-swing:0.3.7")
@@ -0,0 +1,45 @@
package jadx.gui.cache.manager;
import org.jetbrains.annotations.NotNull;
public class CacheEntry implements Comparable<CacheEntry> {
private String project;
private String cache;
private long timestamp;
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public String getCache() {
return cache;
}
public void setCache(String cache) {
this.cache = cache;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
@Override
public int compareTo(@NotNull CacheEntry other) {
// recent entries first
return -Long.compare(timestamp, other.timestamp);
}
@Override
public String toString() {
return "CacheEntry{project=" + project + ", cache=" + cache + "}";
}
}
@@ -0,0 +1,261 @@
package jadx.gui.cache.manager;
import java.io.BufferedReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.data.ProjectData;
import jadx.gui.utils.files.JadxFiles;
public class CacheManager {
private static final Logger LOG = LoggerFactory.getLogger(CacheManager.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Type CACHES_TYPE = new TypeToken<List<CacheEntry>>() {
}.getType();
private final Map<String, CacheEntry> cacheMap;
private final JadxSettings settings;
public CacheManager(JadxSettings settings) {
this.settings = settings;
this.cacheMap = loadCaches();
}
/**
* If project cache is set -> check if cache entry exists for this project.
* If not -> calculate new and add entry.
*/
public Path getCacheDir(JadxProject project, @Nullable String cacheDirStr) {
if (cacheDirStr == null) {
Path newProjectCacheDir = buildCacheDir(project);
addEntry(projectToKey(project), newProjectCacheDir);
return newProjectCacheDir;
}
Path cacheDir = resolveCacheDirStr(cacheDirStr, project.getProjectPath());
return verifyEntry(project, cacheDir);
}
public void projectPathUpdate(JadxProject project, Path newPath) {
if (Objects.equals(project.getProjectPath(), newPath)) {
return;
}
String key = projectToKey(project);
CacheEntry prevEntry = cacheMap.remove(key);
if (prevEntry == null) {
return;
}
CacheEntry newEntry = new CacheEntry();
newEntry.setProject(pathToString(newPath));
newEntry.setCache(prevEntry.getCache());
addEntry(newEntry);
}
public List<CacheEntry> getCachesList() {
List<CacheEntry> list = new ArrayList<>(cacheMap.values());
Collections.sort(list);
return list;
}
public synchronized void removeCacheEntry(CacheEntry entry) {
try {
cacheMap.remove(entry.getProject());
saveCaches(cacheMap);
FileUtils.deleteDirIfExists(Paths.get(entry.getCache()));
} catch (Exception e) {
LOG.error("Failed to remove cache entry: " + entry.getCache(), e);
}
}
private Path resolveCacheDirStr(String cacheDirStr, Path projectPath) {
Path path = Paths.get(cacheDirStr);
if (path.isAbsolute() || projectPath == null) {
return path;
}
return projectPath.resolveSibling(path);
}
public String buildCacheDirStr(Path dir) {
if (Objects.equals(settings.getCacheDir(), ".")) {
return dir.getFileName().toString();
}
return pathToString(dir);
}
private Path buildCacheDir(JadxProject project) {
String cacheDirValue = settings.getCacheDir();
if (Objects.equals(cacheDirValue, ".")) {
return buildLocalCacheDir(project);
}
Path cacheBaseDir = cacheDirValue == null ? JadxFiles.CACHE_DIR : Paths.get(cacheDirValue);
return cacheBaseDir.resolve(buildProjectUniqName(project));
}
private static Path buildLocalCacheDir(JadxProject project) {
Path projectPath = project.getProjectPath();
if (projectPath != null) {
return projectPath.resolveSibling(projectPath.getFileName() + ".cache");
}
List<Path> files = project.getFilePaths();
if (files.isEmpty()) {
throw new JadxRuntimeException("Failed to build local cache dir");
}
Path path = files.stream()
.filter(p -> !p.getFileName().toString().endsWith(".jadx.kts"))
.findFirst()
.orElseGet(() -> files.get(0));
String name = CommonFileUtils.removeFileExtension(path.getFileName().toString());
return path.resolveSibling(name + ".jadx.cache");
}
private Path verifyEntry(JadxProject project, Path cacheDir) {
boolean cacheExists = Files.exists(cacheDir);
String key = projectToKey(project);
CacheEntry entry = cacheMap.get(key);
if (entry == null) {
Path newCacheDir = cacheExists ? cacheDir : buildCacheDir(project);
addEntry(key, newCacheDir);
return newCacheDir;
}
if (entry.getCache().equals(pathToString(cacheDir)) && cacheExists) {
// same and exists
return cacheDir;
}
// remove previous cache dir
FileUtils.deleteDirIfExists(Paths.get(entry.getCache()));
Path newCacheDir = cacheExists ? cacheDir : buildCacheDir(project);
entry.setCache(pathToString(newCacheDir));
entry.setTimestamp(System.currentTimeMillis());
saveCaches(cacheMap);
return newCacheDir;
}
private void addEntry(String projectKey, Path cacheDir) {
CacheEntry entry = new CacheEntry();
entry.setProject(projectKey);
entry.setCache(pathToString(cacheDir));
addEntry(entry);
}
private void addEntry(CacheEntry entry) {
entry.setTimestamp(System.currentTimeMillis());
cacheMap.put(entry.getProject(), entry);
saveCaches(cacheMap);
}
private String projectToKey(JadxProject project) {
Path projectPath = project.getProjectPath();
if (projectPath != null) {
return pathToString(projectPath);
}
return "tmp:" + buildProjectUniqName(project);
}
private static String buildProjectUniqName(JadxProject project) {
return project.getName() + "-" + FileUtils.buildInputsHash(project.getFilePaths());
}
public static String pathToString(Path path) {
try {
return path.toAbsolutePath().normalize().toString();
} catch (Exception e) {
throw new JadxRuntimeException("Failed to expand path: " + path, e);
}
}
private synchronized Map<String, CacheEntry> loadCaches() {
List<CacheEntry> list = null;
if (Files.exists(JadxFiles.CACHES_LIST)) {
try (BufferedReader reader = Files.newBufferedReader(JadxFiles.CACHES_LIST)) {
list = GSON.fromJson(reader, CACHES_TYPE);
} catch (Exception e) {
LOG.warn("Failed to load caches list", e);
}
} else {
return initFromRecentProjects();
}
if (Utils.isEmpty(list)) {
return new HashMap<>();
}
Map<String, CacheEntry> map = new HashMap<>(list.size());
for (CacheEntry entry : list) {
map.put(entry.getProject(), entry);
}
return map;
}
private synchronized void saveCaches(Map<String, CacheEntry> map) {
List<CacheEntry> list = new ArrayList<>(map.values());
Collections.sort(list);
String json = GSON.toJson(list, CACHES_TYPE);
try {
Files.writeString(JadxFiles.CACHES_LIST, json, StandardCharsets.UTF_8,
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to write caches file", e);
}
}
/**
* Load caches info from recent projects list.
* Help for initial migration.
*/
private Map<String, CacheEntry> initFromRecentProjects() {
try {
Map<String, CacheEntry> map = new HashMap<>();
long t = System.currentTimeMillis();
for (Path project : settings.getRecentProjects()) {
try {
ProjectData data = JadxProject.loadProjectData(project);
String cacheDir = data.getCacheDir();
if (cacheDir == null) {
// no cache dir, ignore
continue;
}
Path cachePath = resolveCacheDirStr(cacheDir, project);
if (!Files.isDirectory(cachePath)) {
continue;
}
String key = pathToString(project);
CacheEntry entry = new CacheEntry();
entry.setProject(key);
entry.setCache(pathToString(cachePath));
entry.setTimestamp(t++); // keep projects order
map.put(key, entry);
} catch (Exception e) {
LOG.warn("Failed to load project file: {}", project, e);
}
}
saveCaches(map);
return map;
} catch (Exception e) {
LOG.warn("Failed to fill cache list from recent projects", e);
return new HashMap<>();
}
}
}
@@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -35,6 +36,7 @@ import jadx.api.plugins.utils.CommonFileUtils;
import jadx.core.utils.GsonUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.cache.manager.CacheManager;
import jadx.gui.settings.data.ProjectData;
import jadx.gui.settings.data.TabViewState;
import jadx.gui.ui.MainWindow;
@@ -84,8 +86,10 @@ public class JadxProject {
return null;
}
@Nullable
public Path getProjectPath() {
/**
* @return null if project not saved
*/
public @Nullable Path getProjectPath() {
return projectPath;
}
@@ -100,13 +104,23 @@ public class JadxProject {
}
public void setFilePaths(List<Path> files) {
if (!files.equals(getFilePaths())) {
data.setFiles(files);
String joinedName = files.stream().map(p -> CommonFileUtils.removeFileExtension(p.getFileName().toString()))
.collect(Collectors.joining("_"));
this.name = StringUtils.abbreviate(joinedName, 100);
changed();
if (files.equals(getFilePaths())) {
return;
}
if (files.isEmpty()) {
data.setFiles(files);
name = "";
} else {
Collections.sort(files);
data.setFiles(files);
String joinedName = files.stream()
.map(p -> p.getFileName().toString())
.filter(file -> !file.endsWith(".jadx.kts"))
.map(CommonFileUtils::removeFileExtension)
.collect(Collectors.joining("_"));
name = StringUtils.abbreviate(joinedName, 100);
}
changed();
}
public List<String[]> getTreeExpansions() {
@@ -186,33 +200,30 @@ public class JadxProject {
return data.getPluginOptions().get(key);
}
public @NotNull Path getCacheDir() {
Path cacheDir = data.getCacheDir();
if (cacheDir != null) {
return cacheDir;
private Path cacheDir;
public Path getCacheDir() {
if (cacheDir == null) {
cacheDir = resolveCachePath(data.getCacheDir());
}
return cacheDir;
}
public void resetCacheDir() {
cacheDir = resolveCachePath(null);
}
private Path resolveCachePath(@Nullable String cacheDirStr) {
CacheManager cacheManager = mainWindow.getCacheManager();
Path newCacheDir = cacheManager.getCacheDir(this, cacheDirStr);
String newCacheStr = cacheManager.buildCacheDirStr(newCacheDir);
if (!newCacheStr.equals(cacheDirStr)) {
data.setCacheDir(newCacheStr);
changed();
}
Path newCacheDir = buildCacheDir();
setCacheDir(newCacheDir);
return newCacheDir;
}
public void setCacheDir(Path cacheDir) {
data.setCacheDir(cacheDir);
changed();
}
private Path buildCacheDir() {
if (projectPath != null) {
return projectPath.resolveSibling(projectPath.getFileName() + ".cache");
}
List<Path> files = data.getFiles();
if (!files.isEmpty()) {
Path path = files.get(0);
return path.resolveSibling(path.getFileName() + ".cache");
}
throw new JadxRuntimeException("Failed to build cache dir");
}
public boolean isEnableLiveReload() {
return data.isEnableLiveReload();
}
@@ -273,6 +284,7 @@ public class JadxProject {
}
public void saveAs(Path path) {
mainWindow.getCacheManager().projectPathUpdate(this, path);
setProjectPath(path);
save();
}
@@ -291,10 +303,9 @@ public class JadxProject {
}
public static JadxProject load(MainWindow mainWindow, Path path) {
Path basePath = path.toAbsolutePath().getParent();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
try {
JadxProject project = new JadxProject(mainWindow);
project.data = buildGson(basePath).fromJson(reader, ProjectData.class);
project.data = loadProjectData(path);
project.saved = true;
project.setProjectPath(path);
project.upgrade();
@@ -305,6 +316,15 @@ public class JadxProject {
}
}
public static ProjectData loadProjectData(Path path) {
Path basePath = path.toAbsolutePath().getParent();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
return buildGson(basePath).fromJson(reader, ProjectData.class);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load project file: " + path, e);
}
}
private static Gson buildGson(Path basePath) {
return new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, new RelativePathTypeAdapter(basePath))
@@ -1,6 +1,10 @@
package jadx.gui.settings;
import java.awt.*;
import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Window;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -13,7 +17,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import javax.swing.*;
import javax.swing.JFrame;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.jetbrains.annotations.Nullable;
@@ -44,7 +48,7 @@ public class JadxSettings extends JadxCLIArgs {
private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class);
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int RECENT_PROJECTS_COUNT = 30;
private static final int CURRENT_SETTINGS_VERSION = 18;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
@@ -93,8 +97,10 @@ public class JadxSettings extends JadxCLIArgs {
private String adbDialogHost = "localhost";
private String adbDialogPort = "5037";
private CodeCacheMode codeCacheMode = CodeCacheMode.DISK_WITH_CACHE;
private CodeCacheMode codeCacheMode = CodeCacheMode.DISK;
private UsageCacheMode usageCacheMode = UsageCacheMode.DISK;
private @Nullable String cacheDir = null; // null - default (system), "." - at project dir, other - custom
private boolean jumpOnDoubleClick = true;
/**
@@ -206,8 +212,9 @@ public class JadxSettings extends JadxCLIArgs {
if (projectPath == null) {
return;
}
recentProjects.remove(projectPath);
recentProjects.add(0, projectPath);
Path normPath = projectPath.toAbsolutePath().normalize();
recentProjects.remove(normPath);
recentProjects.add(0, normPath);
int count = recentProjects.size();
if (count > RECENT_PROJECTS_COUNT) {
recentProjects.subList(RECENT_PROJECTS_COUNT, count).clear();
@@ -681,6 +688,14 @@ public class JadxSettings extends JadxCLIArgs {
this.usageCacheMode = usageCacheMode;
}
public @Nullable String getCacheDir() {
return cacheDir;
}
public void setCacheDir(@Nullable String cacheDir) {
this.cacheDir = cacheDir;
}
public boolean isJumpOnDoubleClick() {
return jumpOnDoubleClick;
}
@@ -3,18 +3,16 @@ package jadx.gui.settings;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.prefs.Preferences;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.dirs.ProjectDirectories;
import jadx.core.utils.StringUtils;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxGUI;
import jadx.gui.utils.files.JadxFiles;
/**
* Jadx settings storage. Select first available option:
@@ -38,8 +36,7 @@ public class JadxSettingsStorage {
}
private static Path initConfigFile() {
ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx");
Path confPath = Paths.get(jadxDirs.configDir, "gui.json");
Path confPath = JadxFiles.GUI_CONF;
if (!Files.exists(confPath)) {
copyFromPreferences(confPath);
}
@@ -20,7 +20,7 @@ public class ProjectData {
private JadxCodeData codeData = new JadxCodeData();
private List<TabViewState> openTabs = Collections.emptyList();
private @Nullable Path mappingsPath;
private @Nullable Path cacheDir;
private @Nullable String cacheDir; // don't use relative path adapter
private boolean enableLiveReload = false;
private List<String> searchHistory = new ArrayList<>();
protected Map<String, String> pluginOptions = new HashMap<>();
@@ -78,12 +78,11 @@ public class ProjectData {
this.mappingsPath = mappingsPath;
}
@Nullable
public Path getCacheDir() {
public @Nullable String getCacheDir() {
return cacheDir;
}
public void setCacheDir(Path cacheDir) {
public void setCacheDir(@Nullable String cacheDir) {
this.cacheDir = cacheDir;
}
@@ -57,11 +57,10 @@ import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.plugins.events.JadxEvents;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.usage.UsageCacheMode;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsAdapter;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.settings.ui.cache.CacheSettingsGroup;
import jadx.gui.settings.ui.plugins.PluginsSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorTheme;
@@ -125,6 +124,7 @@ public class JadxSettingsWindow extends JDialog {
groups.add(makeDecompilationGroup());
groups.add(makeDeobfuscationGroup());
groups.add(makeRenameGroup());
groups.add(new CacheSettingsGroup(this));
groups.add(makeAppearanceGroup());
groups.add(makeSearchResGroup());
groups.add(makeProjectGroup());
@@ -402,21 +402,6 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JComboBox<CodeCacheMode> codeCacheModeComboBox = new JComboBox<>(CodeCacheMode.values());
codeCacheModeComboBox.setSelectedItem(settings.getCodeCacheMode());
codeCacheModeComboBox.addActionListener(e -> {
settings.setCodeCacheMode((CodeCacheMode) codeCacheModeComboBox.getSelectedItem());
needReload();
});
String codeCacheModeToolTip = CodeCacheMode.buildToolTip();
JComboBox<UsageCacheMode> usageCacheModeComboBox = new JComboBox<>(UsageCacheMode.values());
usageCacheModeComboBox.setSelectedItem(settings.getUsageCacheMode());
usageCacheModeComboBox.addActionListener(e -> {
settings.setUsageCacheMode((UsageCacheMode) usageCacheModeComboBox.getSelectedItem());
needReload();
});
JCheckBox showInconsistentCode = new JCheckBox();
showInconsistentCode.setSelected(settings.isShowInconsistentCode());
showInconsistentCode.addItemListener(e -> {
@@ -563,8 +548,6 @@ public class JadxSettingsWindow extends JDialog {
NLS.str("preferences.excludedPackages.tooltip"), editExcludedPackages);
other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs);
other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox);
other.addRow(NLS.str("preferences.codeCacheMode"), codeCacheModeToolTip, codeCacheModeComboBox);
other.addRow(NLS.str("preferences.usageCacheMode"), usageCacheModeComboBox);
other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode);
other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode);
other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts);
@@ -712,7 +695,7 @@ public class JadxSettingsWindow extends JDialog {
NLS.str("preferences.copy_message"));
}
void needReload() {
public void needReload() {
needReload = true;
}
@@ -726,6 +709,10 @@ public class JadxSettingsWindow extends JDialog {
return settings.toJadxArgs().makeCodeArgsHash(decompiler);
}
public MainWindow getMainWindow() {
return mainWindow;
}
@Override
public void dispose() {
settings.saveWindowPos(this);
@@ -80,8 +80,8 @@ public class SettingsGroup implements ISettingsGroup {
return title;
}
public JPanel getPanel() {
return panel;
public JPanel getGridPanel() {
return gridPanel;
}
@Override
@@ -0,0 +1,201 @@
package jadx.gui.settings.ui.cache;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.UIManager;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.usage.UsageCacheMode;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.JadxSettingsWindow;
import jadx.gui.settings.ui.SettingsGroup;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.files.JadxFiles;
import jadx.gui.utils.ui.DocumentUpdateListener;
public class CacheSettingsGroup implements ISettingsGroup {
private final String title = NLS.str("preferences.cache");
private final JadxSettingsWindow settingsWindow;
private JTextField customDirField;
private JButton selectDirBtn;
public CacheSettingsGroup(JadxSettingsWindow settingsWindow) {
this.settingsWindow = settingsWindow;
}
@Override
public String getTitle() {
return title;
}
@Override
public JComponent buildComponent() {
JPanel options = new JPanel();
options.setLayout(new BoxLayout(options, BoxLayout.PAGE_AXIS));
options.add(buildBaseOptions());
options.add(buildLocationSelector());
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
mainPanel.add(options, BorderLayout.PAGE_START);
mainPanel.add(buildCachesView(), BorderLayout.CENTER);
return mainPanel;
}
private JPanel buildCachesView() {
CachesTable cachesTable = new CachesTable(settingsWindow.getMainWindow());
JScrollPane scrollPane = new JScrollPane(cachesTable);
cachesTable.setFillsViewportHeight(true);
cachesTable.updateData();
JButton calcUsage = new JButton(NLS.str("preferences.cache.btn.usage"));
calcUsage.addActionListener(ev -> cachesTable.updateSizes());
JButton deleteSelected = new JButton(NLS.str("preferences.cache.btn.delete_selected"));
deleteSelected.addActionListener(ev -> cachesTable.deleteSelected());
JButton deleteAll = new JButton(NLS.str("preferences.cache.btn.delete_all"));
deleteAll.addActionListener(ev -> cachesTable.deleteAll());
JPanel buttons = new JPanel();
buttons.setLayout(new BoxLayout(buttons, BoxLayout.LINE_AXIS));
buttons.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
buttons.add(calcUsage);
buttons.add(Box.createHorizontalGlue());
buttons.add(deleteSelected);
buttons.add(deleteAll);
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.cache.table.title")));
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(buttons, BorderLayout.PAGE_END);
return panel;
}
private JComponent buildLocationSelector() {
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(0, 1));
panel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder(NLS.str("preferences.cache.location")),
BorderFactory.createEmptyBorder(10, 10, 10, 10)));
customDirField = new JTextField();
customDirField.setColumns(10);
customDirField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> {
settingsWindow.getMainWindow().getSettings().setCacheDir(customDirField.getText());
}));
selectDirBtn = new JButton();
selectDirBtn.setIcon(UIManager.getIcon("Tree.closedIcon"));
selectDirBtn.addActionListener(e -> {
FileDialogWrapper fd = new FileDialogWrapper(settingsWindow.getMainWindow(), FileOpenMode.CUSTOM_OPEN);
fd.setFileExtList(Collections.emptyList());
fd.setSelectionMode(JFileChooser.DIRECTORIES_ONLY);
List<Path> paths = fd.show();
if (!paths.isEmpty()) {
String dir = paths.get(0).toAbsolutePath().toString();
customDirField.setText(dir);
settingsWindow.getMainWindow().getSettings().setCacheDir(dir);
}
});
JRadioButton defOpt = new JRadioButton(NLS.str("preferences.cache.location_default"));
defOpt.setToolTipText(JadxFiles.CACHE_DIR.toString());
defOpt.addActionListener(e -> changeCacheLocation(null));
JRadioButton localOpt = new JRadioButton(NLS.str("preferences.cache.location_local"));
localOpt.addActionListener(e -> changeCacheLocation("."));
JRadioButton customOpt = new JRadioButton(NLS.str("preferences.cache.location_custom"));
customOpt.addActionListener(e -> changeCacheLocation(""));
ButtonGroup group = new ButtonGroup();
group.add(defOpt);
group.add(localOpt);
group.add(customOpt);
panel.add(defOpt);
panel.add(localOpt);
JPanel custom = new JPanel();
custom.setLayout(new BoxLayout(custom, BoxLayout.LINE_AXIS));
custom.add(customOpt);
custom.add(Box.createHorizontalStrut(15));
custom.add(customDirField);
custom.add(selectDirBtn);
panel.add(custom);
String cacheDir = settingsWindow.getMainWindow().getSettings().getCacheDir();
if (cacheDir == null) {
defOpt.setSelected(true);
changeCacheLocation(null);
} else if (cacheDir.equals(".")) {
localOpt.setSelected(true);
changeCacheLocation(cacheDir);
} else {
customOpt.setSelected(true);
customDirField.setText(cacheDir);
changeCacheLocation("");
}
JLabel notice = new JLabel(NLS.str("preferences.cache.change_notice"));
notice.setEnabled(false);
panel.add(notice);
return panel;
}
private void changeCacheLocation(@Nullable String locValue) {
boolean custom = Objects.equals(locValue, "");
customDirField.setEnabled(custom);
selectDirBtn.setEnabled(custom);
if (!custom) {
settingsWindow.getMainWindow().getSettings().setCacheDir(locValue);
}
}
private JComponent buildBaseOptions() {
JadxSettings settings = settingsWindow.getMainWindow().getSettings();
JComboBox<CodeCacheMode> codeCacheModeComboBox = new JComboBox<>(CodeCacheMode.values());
codeCacheModeComboBox.setSelectedItem(settings.getCodeCacheMode());
codeCacheModeComboBox.addActionListener(e -> {
settings.setCodeCacheMode((CodeCacheMode) codeCacheModeComboBox.getSelectedItem());
settingsWindow.needReload();
});
JComboBox<UsageCacheMode> usageCacheModeComboBox = new JComboBox<>(UsageCacheMode.values());
usageCacheModeComboBox.setSelectedItem(settings.getUsageCacheMode());
usageCacheModeComboBox.addActionListener(e -> {
settings.setUsageCacheMode((UsageCacheMode) usageCacheModeComboBox.getSelectedItem());
settingsWindow.needReload();
});
SettingsGroup group = new SettingsGroup(title);
group.addRow(NLS.str("preferences.codeCacheMode"), CodeCacheMode.buildToolTip(), codeCacheModeComboBox);
group.addRow(NLS.str("preferences.usageCacheMode"), usageCacheModeComboBox);
return group.buildComponent();
}
}
@@ -0,0 +1,152 @@
package jadx.gui.settings.ui.cache;
import java.awt.Dimension;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.gui.cache.manager.CacheManager;
import jadx.gui.settings.JadxProject;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.MousePressedHandler;
public class CachesTable extends JTable {
private static final long serialVersionUID = 5984107298264276049L;
private static final Logger LOG = LoggerFactory.getLogger(CachesTable.class);
private final MainWindow mainWindow;
private final CachesTableModel dataModel;
public CachesTable(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.dataModel = new CachesTableModel();
setModel(dataModel);
setDefaultRenderer(Object.class, new CachesTableRenderer());
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
setShowHorizontalLines(true);
setDragEnabled(false);
setColumnSelectionAllowed(false);
setAutoscrolls(true);
setFocusable(false);
addMouseListener(new MousePressedHandler(ev -> {
int row = rowAtPoint(ev.getPoint());
if (row != -1) {
dataModel.changeSelection(row);
UiUtils.uiRun(this::updateUI);
}
}));
}
public void updateData() {
List<TableRow> rows = mainWindow.getCacheManager().getCachesList().stream()
.map(TableRow::new)
.collect(Collectors.toList());
updateRows(rows);
}
public void reloadData() {
Map<String, String> prevUsageMap = dataModel.getRows().stream()
.collect(Collectors.toMap(TableRow::getProject, TableRow::getUsage));
List<TableRow> rows = mainWindow.getCacheManager().getCachesList().stream()
.map(TableRow::new)
.peek(r -> r.setUsage(Utils.getOrElse(prevUsageMap.get(r.getProject()), "-")))
.collect(Collectors.toList());
updateRows(rows);
}
private void updateRows(List<TableRow> rows) {
dataModel.setRows(rows);
// fix allocated space for default 20 rows
int width = getPreferredSize().width;
int height = rows.size() * getRowHeight();
setPreferredScrollableViewportSize(new Dimension(width, height));
UiUtils.uiRun(this::updateUI);
}
public void updateSizes() {
List<Runnable> list = dataModel.getRows().stream()
.map(row -> (Runnable) () -> calcSize(row))
.collect(Collectors.toList());
mainWindow.getBackgroundExecutor().execute(
NLS.str("preferences.cache.task.usage"),
list,
status -> updateUI());
}
private void calcSize(TableRow row) {
String cacheDir = row.getCacheEntry().getCache();
try {
File dir = new File(cacheDir);
if (dir.exists()) {
long size = FileUtils.sizeOfDirectory(dir);
row.setUsage(FileUtils.byteCountToDisplaySize(size));
} else {
row.setUsage("not found");
}
} catch (Exception e) {
LOG.warn("Failed to calculate size of directory: {}", cacheDir, e);
row.setUsage("error");
}
}
public void deleteSelected() {
delete(ListUtils.filter(dataModel.getRows(), TableRow::isSelected));
}
public void deleteAll() {
delete(dataModel.getRows());
}
private void delete(List<TableRow> rows) {
// force reload if cache for current project is deleted
boolean reload = searchCurrentProject(rows);
List<Runnable> list = rows.stream()
.map(TableRow::getCacheEntry)
.map(entry -> (Runnable) () -> mainWindow.getCacheManager().removeCacheEntry(entry))
.collect(Collectors.toList());
mainWindow.getBackgroundExecutor().execute(
NLS.str("preferences.cache.task.delete"),
list,
status -> {
reloadData();
if (reload) {
mainWindow.reopen();
}
});
}
private boolean searchCurrentProject(List<TableRow> rows) {
JadxProject project = mainWindow.getProject();
if (!project.getFilePaths().isEmpty()) {
String cacheStr = CacheManager.pathToString(project.getCacheDir());
for (TableRow row : rows) {
if (row.getCacheEntry().getCache().equals(cacheStr)) {
project.resetCacheDir();
LOG.debug("Found current project in cache delete list -> request full reload");
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,57 @@
package jadx.gui.settings.ui.cache;
import java.util.Collections;
import java.util.List;
import javax.swing.table.AbstractTableModel;
import jadx.gui.utils.NLS;
public class CachesTableModel extends AbstractTableModel {
private static final long serialVersionUID = -7725573085995496397L;
private static final String[] COLUMN_NAMES = {
NLS.str("preferences.cache.table.project"),
NLS.str("preferences.cache.table.size")
};
private transient List<TableRow> rows = Collections.emptyList();
public void setRows(List<TableRow> list) {
this.rows = list;
}
public List<TableRow> getRows() {
return rows;
}
@Override
public int getRowCount() {
return rows.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(int index) {
return COLUMN_NAMES[index];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return TableRow.class;
}
@Override
public TableRow getValueAt(int rowIndex, int columnIndex) {
return rows.get(rowIndex);
}
public void changeSelection(int idx) {
TableRow row = rows.get(idx);
row.setSelected(!row.isSelected());
}
}
@@ -0,0 +1,40 @@
package jadx.gui.settings.ui.cache;
import java.awt.Component;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
public class CachesTableRenderer implements TableCellRenderer {
private final JLabel label;
public CachesTableRenderer() {
label = new JLabel();
label.setOpaque(true);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
TableRow obj = (TableRow) value;
switch (column) {
case 0:
label.setText(obj.getProject());
break;
case 1:
label.setText(obj.getUsage());
break;
}
label.setToolTipText(obj.getCacheEntry().getCache());
if (obj.isSelected()) {
label.setBackground(table.getSelectionBackground());
label.setForeground(table.getSelectionForeground());
} else {
label.setBackground(table.getBackground());
label.setForeground(table.getForeground());
}
return label;
}
}
@@ -0,0 +1,52 @@
package jadx.gui.settings.ui.cache;
import java.nio.file.Paths;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.gui.cache.manager.CacheEntry;
final class TableRow {
private final CacheEntry cacheEntry;
private final String project;
private String usage;
private boolean selected = false;
public TableRow(CacheEntry cacheEntry) {
this.cacheEntry = cacheEntry;
this.project = cutProjectName(cacheEntry.getProject());
this.usage = "-";
}
private String cutProjectName(String project) {
if (project.startsWith("tmp:")) {
int hashStart = project.lastIndexOf('-');
int endIdx = hashStart != -1 ? hashStart : project.length();
return project.substring(4, endIdx) + " (Temp)";
}
return CommonFileUtils.removeFileExtension(Paths.get(project).getFileName().toString());
}
public CacheEntry getCacheEntry() {
return cacheEntry;
}
public String getProject() {
return project;
}
public String getUsage() {
return usage;
}
public void setUsage(String usage) {
this.usage = usage;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
}
@@ -104,9 +104,10 @@ class PluginsSettingsGroup implements ISettingsGroup {
JScrollPane scrollPane = new JScrollPane(pluginsList);
detailsPanel = new JPanel(new BorderLayout(5, 5));
detailsPanel.setBorder(BorderFactory.createTitledBorder("Plugin details"));
detailsPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder(NLS.str("preferences.plugins.details")),
BorderFactory.createEmptyBorder(10, 10, 10, 10)));
detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.PAGE_AXIS));
detailsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JSplitPane splitPanel = new JSplitPane();
splitPanel.setBorder(BorderFactory.createEmptyBorder(10, 2, 2, 2));
@@ -91,6 +91,7 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.cache.manager.CacheManager;
import jadx.gui.device.debugger.BreakpointManager;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.jobs.DecompileTask;
@@ -179,6 +180,7 @@ public class MainWindow extends JFrame {
private final transient JadxWrapper wrapper;
private final transient JadxSettings settings;
private final transient CacheObject cacheObject;
private final transient CacheManager cacheManager;
private final transient BackgroundExecutor backgroundExecutor;
private transient @NotNull JadxProject project;
@@ -231,6 +233,7 @@ public class MainWindow extends JFrame {
this.wrapper = new JadxWrapper(this);
this.liveReloadWorker = new LiveReloadWorker(this);
this.renameMappings = new RenameMappingsGui(this);
this.cacheManager = new CacheManager(settings);
resetCache();
FontUtils.registerBundledFonts();
@@ -655,6 +658,15 @@ public class MainWindow extends JFrame {
backgroundExecutor.execute(new DecompileTask(this));
}
public void resetCodeCache() {
Path cacheDir = project.getCacheDir();
project.resetCacheDir();
backgroundExecutor.execute(
NLS.str("preferences.cache.task.delete"),
() -> FileUtils.deleteDirIfExists(cacheDir),
status -> reopen());
}
public void cancelBackgroundJobs() {
backgroundExecutor.cancelAll();
}
@@ -1019,6 +1031,10 @@ public class MainWindow extends JFrame {
decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all"));
decompileAllAction.setIcon(ICON_DECOMPILE_ALL);
ActionHandler resetCacheAction = new ActionHandler(ev -> resetCodeCache());
resetCacheAction.setNameAndDesc(NLS.str("menu.reset_cache"));
resetCacheAction.setIcon(Icons.RESET);
Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -1130,6 +1146,7 @@ public class MainWindow extends JFrame {
JMenu tools = new JMenu(NLS.str("menu.tools"));
tools.setMnemonic(KeyEvent.VK_T);
tools.add(decompileAllAction);
tools.add(resetCacheAction);
tools.add(deobfMenuItem);
tools.add(quarkAction);
tools.add(openDeviceAction);
@@ -1658,6 +1675,10 @@ public class MainWindow extends JFrame {
return renameMappings;
}
public CacheManager getCacheManager() {
return cacheManager;
}
/**
* Events instance if decompiler not yet available
*/
@@ -33,4 +33,5 @@ public class Icons {
public static final ImageIcon RUN = UiUtils.openSvgIcon("ui/run");
public static final ImageIcon CHECK = UiUtils.openSvgIcon("ui/checkConstraint");
public static final ImageIcon FORMAT = UiUtils.openSvgIcon("ui/toolWindowMessages");
public static final ImageIcon RESET = UiUtils.openSvgIcon("ui/reset");
}
@@ -0,0 +1,23 @@
package jadx.gui.utils.files;
import java.nio.file.Path;
import java.nio.file.Paths;
import dev.dirs.ProjectDirectories;
import jadx.core.utils.files.FileUtils;
public class JadxFiles {
private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx");
private static final String CONFIG_DIR = DIRS.configDir;
public static final Path GUI_CONF = Paths.get(CONFIG_DIR, "gui.json");
public static final Path CACHES_LIST = Paths.get(CONFIG_DIR, "caches.json");
public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
static {
FileUtils.makeDirs(Paths.get(CONFIG_DIR));
}
}
@@ -0,0 +1,19 @@
package jadx.gui.utils.ui;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.function.Consumer;
public class MousePressedHandler extends MouseAdapter {
private final Consumer<MouseEvent> listener;
public MousePressedHandler(Consumer<MouseEvent> listener) {
this.listener = listener;
}
@Override
public void mousePressed(MouseEvent ev) {
listener.accept(ev);
}
}
@@ -17,6 +17,7 @@ menu.comment_search=Kommentar suchen
menu.tools=Tools
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
#menu.reset_cache=Reset code cache
menu.deobfuscation=Deobfuskierung
menu.log=Log-Anzeige
menu.help=Hilfe
@@ -229,10 +230,26 @@ preferences.res_skip_file=Dateien überspringen (MB)
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.details=Plugin details
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=Bitte Datei öffnen
msg.saving_sources=Quelltexte speichern
msg.language_changed_title=Sprache speichern
@@ -17,6 +17,7 @@ menu.comment_search=Comment search
menu.tools=Tools
menu.plugins=Plugins
menu.decompile_all=Decompile all classes
menu.reset_cache=Reset code cache
menu.deobfuscation=Deobfuscation
menu.log=Log Viewer
menu.help=Help
@@ -229,10 +230,26 @@ preferences.plugins.location_id_label=Location id:
preferences.plugins.plugin_jar=Select Plugin jar
preferences.plugins.plugin_jar_label=or
preferences.plugins.update_all=Update All
preferences.plugins.details=Plugin details
preferences.plugins.task.installing=Installing plugin
preferences.plugins.task.uninstalling=Uninstalling plugin
preferences.plugins.task.updating=Updating plugins
preferences.cache=Cache
preferences.cache.location=Cache location
preferences.cache.location_default=App cache system directory
preferences.cache.location_local=Same directory as project file
preferences.cache.location_custom=Custom location:
preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
preferences.cache.table.title=Caches list
preferences.cache.table.project=Cache for project
preferences.cache.table.size=Disk usage
preferences.cache.btn.usage=Calculate usage
preferences.cache.btn.delete_selected=Delete Selected
preferences.cache.btn.delete_all=Delete All
preferences.cache.task.usage=Calculating cache size
preferences.cache.task.delete=Deleting caches
msg.open_file=Please open file
msg.saving_sources=Saving sources
msg.language_changed_title=Language changed
@@ -17,6 +17,7 @@ menu.class_search=Buscar clase
menu.tools=Herramientas
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
#menu.reset_cache=Reset code cache
menu.deobfuscation=Desofuscación
menu.log=Visor log
menu.help=Ayuda
@@ -229,10 +230,26 @@ preferences.reset_title=Reestablecer preferencias
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.details=Plugin details
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=Por favor, abra un archivo
msg.saving_sources=Guardando fuente
msg.language_changed_title=Idioma cambiado
@@ -17,6 +17,7 @@ menu.comment_search=주석 검색
menu.tools=도구
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
#menu.reset_cache=Reset code cache
menu.deobfuscation=난독화 해제
menu.log=로그 뷰어
menu.help=도움말
@@ -229,10 +230,26 @@ preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB)
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.details=Plugin details
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=파일을 여십시오
msg.saving_sources=소스 저장 중
msg.language_changed_title=언어 변경됨
@@ -17,6 +17,7 @@ menu.comment_search=Busca por comentário
menu.tools=Ferramentas
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
#menu.reset_cache=Reset code cache
menu.deobfuscation=Desofuscar
menu.log=Visualizador de log
menu.help=Ajuda
@@ -229,10 +230,26 @@ preferences.res_skip_file=Pular arquivos excedidos
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.details=Plugin details
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=Abra um arquivo
msg.saving_sources=Salvando recursos
msg.language_changed_title=Idioma alterado
@@ -17,6 +17,7 @@ menu.comment_search=Поиск комментариев
menu.tools=Инструменты
#menu.plugins=Plugins
#menu.decompile_all=Decompile all classes
#menu.reset_cache=Reset code cache
menu.deobfuscation=Деобфускация
menu.log=Просмотр логов
menu.help=Помощь
@@ -229,10 +230,26 @@ preferences.res_skip_file=Пропускать ресурсы больше че
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
#preferences.plugins.update_all=Update All
#preferences.plugins.details=Plugin details
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=Пожалуйста, откройте файл
msg.saving_sources=Сохранение ресурсов
msg.language_changed_title=Язык изменен
@@ -17,6 +17,7 @@ menu.comment_search=注释搜索
menu.tools=工具
menu.plugins=插件
menu.decompile_all=反编译所有类
#menu.reset_cache=Reset code cache
menu.deobfuscation=反混淆
menu.log=日志查看器
menu.help=帮助
@@ -229,10 +230,26 @@ preferences.plugins.location_id_label=位置ID
preferences.plugins.plugin_jar=选择插件 jar
preferences.plugins.plugin_jar_label=
preferences.plugins.update_all=更新所有
#preferences.plugins.details=Plugin details
preferences.plugins.task.installing=安装插件中
preferences.plugins.task.uninstalling=卸载插件中
preferences.plugins.task.updating=更新插件中
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=请打开文件
msg.saving_sources=正在导出源代码
msg.language_changed_title=语言已更改
@@ -17,6 +17,7 @@ menu.comment_search=註解搜尋
menu.tools=工具
menu.plugins=外掛程式
menu.decompile_all=反編譯所有類別
#menu.reset_cache=Reset code cache
menu.deobfuscation=去模糊化
menu.log=記錄檔檢視器
menu.help=幫助
@@ -229,10 +230,26 @@ preferences.plugins.location_id_label=位置 id
preferences.plugins.plugin_jar=選擇外掛程式 jar
preferences.plugins.plugin_jar_label=
preferences.plugins.update_all=全部更新
#preferences.plugins.details=Plugin details
preferences.plugins.task.installing=正在安裝外掛程式
preferences.plugins.task.uninstalling=正在解除安裝外掛程式
preferences.plugins.task.updating=正在更新外掛程式
#preferences.cache=Cache
#preferences.cache.location=Cache location
#preferences.cache.location_default=App cache system directory
#preferences.cache.location_local=Same directory as project file
#preferences.cache.location_custom=Custom location:
#preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset
#preferences.cache.table.title=Caches list
#preferences.cache.table.project=Cache for project
#preferences.cache.table.size=Disk usage
#preferences.cache.btn.usage=Calculate usage
#preferences.cache.btn.delete_selected=Delete Selected
#preferences.cache.btn.delete_all=Delete All
#preferences.cache.task.usage=Calculating cache size
#preferences.cache.task.delete=Deleting caches
msg.open_file=請開啟檔案
msg.saving_sources=正在儲存原始碼
msg.language_changed_title=已更改語言
@@ -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="#6E6E6E" fill-rule="evenodd" transform="translate(2 1)">
<polygon points="0 .1 4 4.1 0 8.1" transform="matrix(-1 0 0 1 4 0)"/>
<path d="M4,11 L8.00000048,11 C9.65685473,11 11.0000005,9.65685425 11.0000005,8 C11.0000005,6.34314575 9.65685473,5 8.00000048,5 L4,5 L4,3 L8,3 C10.7614237,3 13,5.23857625 13,8 C13,10.7614237 10.7614237,13 8,13 L4,13 L4,11 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 637 B