feat(gui): add option to set cache location, view/delete exists caches (#1941)
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user