提交 b09c7ba6 编写于 作者: A Ahmed Ashour 提交者: skylot

feat(gui): support project (#526) (PR #543)

上级 ec66476a
......@@ -231,10 +231,6 @@ public class JadxCLIArgs {
return deobfuscationUseSourceNameAsAlias;
}
public boolean escapeUnicode() {
return escapeUnicode;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
......
......@@ -26,7 +26,7 @@ public class JadxGUI {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
NLS.setLocale(settings.getLangLocale());
SwingUtilities.invokeLater(new MainWindow(settings)::open);
SwingUtilities.invokeLater(new MainWindow(settings)::init);
} catch (Exception e) {
LOG.error("Error: {}", e.getMessage(), e);
System.exit(1);
......
......@@ -47,27 +47,24 @@ public class JadxWrapper {
}
public void saveAll(final File dir, final ProgressMonitor progressMonitor) {
Runnable save = new Runnable() {
@Override
public void run() {
try {
decompiler.getArgs().setRootDir(dir);
ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor();
ex.shutdown();
while (ex.isTerminating()) {
long total = ex.getTaskCount();
long done = ex.getCompletedTaskCount();
progressMonitor.setProgress((int) (done * 100.0 / total));
Thread.sleep(500);
}
progressMonitor.close();
LOG.info("decompilation complete, freeing memory ...");
decompiler.getClasses().forEach(JavaClass::unload);
LOG.info("done");
} catch (InterruptedException e) {
LOG.error("Save interrupted", e);
Thread.currentThread().interrupt();
Runnable save = () -> {
try {
decompiler.getArgs().setRootDir(dir);
ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor();
ex.shutdown();
while (ex.isTerminating()) {
long total = ex.getTaskCount();
long done = ex.getCompletedTaskCount();
progressMonitor.setProgress((int) (done * 100.0 / total));
Thread.sleep(500);
}
progressMonitor.close();
LOG.info("decompilation complete, freeing memory ...");
decompiler.getClasses().forEach(JavaClass::unload);
LOG.info("done");
} catch (InterruptedException e) {
LOG.error("Save interrupted", e);
Thread.currentThread().interrupt();
}
};
new Thread(save).start();
......
......@@ -58,8 +58,7 @@ public class BackgroundWorker extends SwingWorker<Void, Void> {
if (searchIndex != null && searchIndex.getSkippedCount() > 0) {
LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}",
searchIndex.getSkippedCount(), Utils.memoryInfo());
String msg = NLS.str("message.indexingClassesSkipped");
msg = String.format(msg, searchIndex.getSkippedCount());
String msg = NLS.str("message.indexingClassesSkipped", searchIndex.getSkippedCount());
JOptionPane.showMessageDialog(null, msg);
}
} catch (Exception e) {
......
package jadx.gui.settings;
import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.gui.utils.PathTypeAdapter;
public class JadxProject {
private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class);
private static final int CURRENT_SETTINGS_VERSION = 0;
public static final String PROJECT_EXTENSION = "jadx";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
.create();
private transient JadxSettings settings;
private transient String name = "New Project";
private transient Path projectPath;
private List<Path> filesPath;
private transient boolean saved;
private transient boolean initial = true;
private int projectVersion = 0;
public JadxProject(JadxSettings settings) {
this.settings = settings;
}
public Path getProjectPath() {
return projectPath;
}
private void setProjectPath(Path projectPath) {
this.projectPath = projectPath;
if (projectVersion != CURRENT_SETTINGS_VERSION) {
upgradeSettings(projectVersion);
}
name = projectPath.getFileName().toString();
name = name.substring(0, name.lastIndexOf('.'));
changed();
}
public Path getFilePath() {
return filesPath == null ? null : filesPath.get(0);
}
public void setFilePath(Path filePath) {
if (!filePath.equals(getFilePath())) {
this.filesPath = Arrays.asList(filePath);
changed();
}
}
private void changed() {
if (settings.isAutoSaveProject()) {
save();
}
else {
saved = false;
}
initial = false;
}
public String getName() {
return name;
}
public boolean isSaved() {
return saved;
}
public boolean isInitial() {
return initial;
}
public void saveAs(Path path) {
setProjectPath(path);
save();
}
public void save() {
try (BufferedWriter writer = Files.newBufferedWriter(getProjectPath())) {
writer.write(GSON.toJson(this));
saved = true;
} catch (Exception e) {
LOG.error("Error saving project", e);
}
}
public static JadxProject from(Path path, JadxSettings settings) {
try {
List<String> lines = Files.readAllLines(path);
if (!lines.isEmpty()) {
JadxProject project = GSON.fromJson(lines.get(0), JadxProject.class);
project.settings = settings;
project.setProjectPath(path);
project.saved = true;
return project;
}
} catch (Exception e) {
LOG.error("Error loading project", e);
}
return null;
}
private void upgradeSettings(int fromVersion) {
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
if (fromVersion == 0) {
fromVersion++;
}
projectVersion = CURRENT_SETTINGS_VERSION;
save();
}
}
......@@ -4,6 +4,8 @@ import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Window;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
......@@ -27,25 +29,27 @@ import jadx.gui.utils.Utils;
public class JadxSettings extends JadxCLIArgs {
private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class);
private static final String USER_HOME = System.getProperty("user.home");
private static final int RECENT_FILES_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 8;
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 9;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
static final Set<String> SKIP_FIELDS = new HashSet<>(Arrays.asList(
"files", "input", "outDir", "outDirSrc", "outDirRes", "verbose", "printVersion", "printHelp"
));
private String lastOpenFilePath = USER_HOME;
private String lastSaveFilePath = USER_HOME;
private Path lastSaveProjectPath = USER_HOME;
private Path lastOpenFilePath = USER_HOME;
private Path lastSaveFilePath = USER_HOME;
private boolean flattenPackage = false;
private boolean checkForUpdates = false;
private List<String> recentFiles = new ArrayList<>();
private List<Path> recentProjects = new ArrayList<>();
private String fontStr = "";
private String editorThemePath = "";
private LangLocale langLocale = NLS.defaultLocale();
private boolean autoStartJobs = false;
protected String excludedPackages = "";
private boolean autoSaveProject = false;
private boolean showHeapUsageBar = true;
......@@ -84,20 +88,29 @@ public class JadxSettings extends JadxCLIArgs {
}
}
public String getLastOpenFilePath() {
public Path getLastOpenFilePath() {
return lastOpenFilePath;
}
public void setLastOpenFilePath(String lastOpenFilePath) {
public void setLastOpenFilePath(Path lastOpenFilePath) {
this.lastOpenFilePath = lastOpenFilePath;
partialSync(settings -> settings.lastOpenFilePath = JadxSettings.this.lastOpenFilePath);
}
public String getLastSaveFilePath() {
public Path getLastSaveProjectPath() {
return lastSaveProjectPath;
}
public Path getLastSaveFilePath() {
return lastSaveFilePath;
}
public void setLastSaveFilePath(String lastSaveFilePath) {
public void setLastSaveProjectPath(Path lastSaveProjectPath) {
this.lastSaveProjectPath = lastSaveProjectPath;
partialSync(settings -> settings.lastSaveProjectPath = JadxSettings.this.lastSaveProjectPath);
}
public void setLastSaveFilePath(Path lastSaveFilePath) {
this.lastSaveFilePath = lastSaveFilePath;
partialSync(settings -> settings.lastSaveFilePath = JadxSettings.this.lastSaveFilePath);
}
......@@ -120,18 +133,18 @@ public class JadxSettings extends JadxCLIArgs {
sync();
}
public Iterable<String> getRecentFiles() {
return recentFiles;
public Iterable<Path> getRecentProjects() {
return recentProjects;
}
public void addRecentFile(String filePath) {
recentFiles.remove(filePath);
recentFiles.add(0, filePath);
int count = recentFiles.size();
if (count > RECENT_FILES_COUNT) {
recentFiles.subList(RECENT_FILES_COUNT, count).clear();
public void addRecentProject(Path projectPath) {
recentProjects.remove(projectPath);
recentProjects.add(0, projectPath);
int count = recentProjects.size();
if (count > RECENT_PROJECTS_COUNT) {
recentProjects.subList(RECENT_PROJECTS_COUNT, count).clear();
}
partialSync(settings -> settings.recentFiles = recentFiles);
partialSync(settings -> settings.recentProjects = recentProjects);
}
public void saveWindowPos(Window window) {
......@@ -265,6 +278,14 @@ public class JadxSettings extends JadxCLIArgs {
this.autoStartJobs = autoStartJobs;
}
public boolean isAutoSaveProject() {
return autoSaveProject;
}
public void setAutoSaveProject(boolean autoSaveProject) {
this.autoSaveProject = autoSaveProject;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
}
......@@ -343,6 +364,10 @@ public class JadxSettings extends JadxCLIArgs {
outDir = null;
outDirSrc = null;
outDirRes = null;
fromVersion++;
}
if (fromVersion == 8) {
fromVersion++;
}
settingsVersion = CURRENT_SETTINGS_VERSION;
sync();
......
package jadx.gui.settings;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.JadxGUI;
import jadx.gui.utils.PathTypeAdapter;
public class JadxSettingsAdapter {
......@@ -34,7 +37,10 @@ public class JadxSettingsAdapter {
return false;
}
};
private static final GsonBuilder GSON_BUILDER = new GsonBuilder().setExclusionStrategies(EXCLUDE_FIELDS);
private static final GsonBuilder GSON_BUILDER = new GsonBuilder()
.setExclusionStrategies(EXCLUDE_FIELDS)
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
;
private static final Gson GSON = GSON_BUILDER.create();
private JadxSettingsAdapter() {
......
......@@ -52,6 +52,7 @@ public class JadxSettingsWindow extends JDialog {
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
panel.add(makeDeobfuscationGroup());
panel.add(makeDecompilationGroup());
panel.add(makeProjectGroup());
panel.add(makeEditorGroup());
panel.add(makeOtherGroup());
......@@ -168,6 +169,18 @@ public class JadxSettingsWindow extends JDialog {
connectedComponents.forEach(comp -> comp.setEnabled(enabled));
}
private SettingsGroup makeProjectGroup() {
JCheckBox autoSave = new JCheckBox();
autoSave.setSelected(settings.isAutoSaveProject());
autoSave.addItemListener(e ->
settings.setAutoSaveProject(e.getStateChange() == ItemEvent.SELECTED));
SettingsGroup group = new SettingsGroup(NLS.str("preferences.project"));
group.addRow(NLS.str("preferences.autoSave"), autoSave);
return group;
}
private SettingsGroup makeEditorGroup() {
JButton fontBtn = new JButton(NLS.str("preferences.select_font"));
......@@ -186,9 +199,9 @@ public class JadxSettingsWindow extends JDialog {
mainWindow.loadSettings();
});
SettingsGroup other = new SettingsGroup(NLS.str("preferences.editor"));
JLabel fontLabel = other.addRow(getFontLabelStr(), fontBtn);
other.addRow(NLS.str("preferences.theme"), themesCbx);
SettingsGroup group = new SettingsGroup(NLS.str("preferences.editor"));
JLabel fontLabel = group.addRow(getFontLabelStr(), fontBtn);
group.addRow(NLS.str("preferences.theme"), themesCbx);
fontBtn.addMouseListener(new MouseAdapter() {
@Override
......@@ -205,7 +218,7 @@ public class JadxSettingsWindow extends JDialog {
}
}
});
return other;
return group;
}
private String getFontLabelStr() {
......@@ -263,7 +276,7 @@ public class JadxSettingsWindow extends JDialog {
autoStartJobs.addItemListener(e -> settings.setAutoStartJobs(e.getStateChange() == ItemEvent.SELECTED));
JCheckBox escapeUnicode = new JCheckBox();
escapeUnicode.setSelected(settings.escapeUnicode());
escapeUnicode.setSelected(settings.isEscapeUnicode());
escapeUnicode.addItemListener(e -> {
settings.setEscapeUnicode(e.getStateChange() == ItemEvent.SELECTED);
needReload();
......@@ -333,12 +346,12 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
SettingsGroup other = new SettingsGroup(NLS.str("preferences.other"));
other.addRow(NLS.str("preferences.language"), languageCbx);
other.addRow(NLS.str("preferences.check_for_updates"), update);
other.addRow(NLS.str("preferences.cfg"), cfg);
other.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
return other;
SettingsGroup group = new SettingsGroup(NLS.str("preferences.other"));
group.addRow(NLS.str("preferences.language"), languageCbx);
group.addRow(NLS.str("preferences.check_for_updates"), update);
group.addRow(NLS.str("preferences.cfg"), cfg);
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
return group;
}
private void needReload() {
......
......@@ -77,15 +77,15 @@ public class ApkSignature extends JNode {
final String err = NLS.str("apkSignature.errors");
final String warn = NLS.str("apkSignature.warnings");
final String sigSucc = NLS.str("apkSignature.signatureSuccess");
final String sigFail = NLS.str("apkSignature.signatureFailed");
final String sigSuccKey = "apkSignature.signatureSuccess";
final String sigFailKey = "apkSignature.signatureFailed";
writeIssues(builder, err, result.getErrors());
writeIssues(builder, warn, result.getWarnings());
if (!result.getV1SchemeSigners().isEmpty()) {
builder.append("<h2>");
builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1));
builder.escape(NLS.str(result.isVerifiedUsingV1Scheme() ? sigSuccKey : sigFailKey, 1));
builder.append("</h2>\n");
builder.append("<blockquote>");
......@@ -106,7 +106,7 @@ public class ApkSignature extends JNode {
}
if (!result.getV2SchemeSigners().isEmpty()) {
builder.append("<h2>");
builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2));
builder.escape(NLS.str(result.isVerifiedUsingV2Scheme() ? sigSuccKey : sigFailKey, 2));
builder.append("</h2>\n");
builder.append("<blockquote>");
......
......@@ -232,11 +232,10 @@ public abstract class CommonSearchDialog extends JDialog {
}
protected void updateProgressLabel() {
String statusText = String.format(
NLS.str("search_dialog.info_label"),
resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(),
resultsModel.getResultCount()
String statusText = NLS.str("search_dialog.info_label",
resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(),
resultsModel.getResultCount()
);
resultsInfoLabel.setText(statusText);
}
......
......@@ -25,11 +25,9 @@ public class HeapUsageBar extends JProgressBar implements ActionListener {
private final transient Runtime runtime = Runtime.getRuntime();
private final transient Timer timer;
private final String textFormat;
private final double maxGB;
public HeapUsageBar() {
this.textFormat = NLS.str("heapUsage.text");
setBorderPainted(false);
setStringPainted(true);
setValue(10);
......@@ -54,7 +52,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener {
long used = runtime.totalMemory() - runtime.freeMemory();
int usedKB = (int) (used / 1024);
setValue(usedKB);
setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB));
setString(NLS.str("heapUsage.text", (usedKB / TWO_TO_20), maxGB));
if ((used + Utils.MIN_FREE_MEMORY) > runtime.maxMemory()) {
setForeground(RED);
......
......@@ -62,7 +62,7 @@ public class MainDropTarget implements DropTargetListener {
if (!transferData.isEmpty()) {
dtde.dropComplete(true);
// load first file
mainWindow.openFile(transferData.get(0));
mainWindow.open(transferData.get(0).toPath());
}
} catch (Exception e) {
LOG.error("File drop operation failed", e);
......
package jadx.gui.ui;
import javax.swing.*;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import static javax.swing.KeyStroke.getKeyStroke;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.DisplayMode;
import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.event.ActionEvent;
......@@ -21,12 +16,51 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -37,6 +71,7 @@ import jadx.gui.JadxWrapper;
import jadx.gui.jobs.BackgroundWorker;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsWindow;
import jadx.gui.treemodel.ApkSignature;
......@@ -56,8 +91,6 @@ import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.Utils;
import static javax.swing.KeyStroke.getKeyStroke;
@SuppressWarnings("serial")
public class MainWindow extends JFrame {
private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class);
......@@ -86,6 +119,9 @@ public class MainWindow extends JFrame {
private final transient JadxWrapper wrapper;
private final transient JadxSettings settings;
private final transient CacheObject cacheObject;
private transient JadxProject project;
private transient Action newProjectAction;
private transient Action saveProjectAction;
private JPanel mainPanel;
......@@ -119,20 +155,26 @@ public class MainWindow extends JFrame {
Utils.setWindowIcons(this);
loadSettings();
checkForUpdate();
newProject();
}
public void open() {
public void init() {
pack();
setLocationAndPosition();
heapUsageBar.setVisible(settings.isShowHeapUsageBar());
setVisible(true);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
closeWindow();
}
});
if (settings.getFiles().isEmpty()) {
openFile();
openFileOrProject();
} else {
openFile(new File(settings.getFiles().get(0)));
open(Paths.get(settings.getFiles().get(0)));
}
}
......@@ -146,7 +188,7 @@ public class MainWindow extends JFrame {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateLink.setText(String.format(NLS.str("menu.update_label"), r.getName()));
updateLink.setText(NLS.str("menu.update_label", r.getName()));
updateLink.setVisible(true);
}
});
......@@ -154,33 +196,160 @@ public class MainWindow extends JFrame {
});
}
public void openFile() {
public void openFileOrProject() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setAcceptAllFileFilterUsed(true);
String[] exts = {"apk", "dex", "jar", "class", "zip", "aar", "arsc"};
String[] exts = {JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "zip", "aar", "arsc"};
String description = "supported files: " + Arrays.toString(exts).replace('[', '(').replace(']', ')');
fileChooser.setFileFilter(new FileNameExtensionFilter(description, exts));
fileChooser.setToolTipText(NLS.str("file.open_action"));
String currentDirectory = settings.getLastOpenFilePath();
if (!currentDirectory.isEmpty()) {
fileChooser.setCurrentDirectory(new File(currentDirectory));
Path currentDirectory = settings.getLastOpenFilePath();
if (currentDirectory != null) {
fileChooser.setCurrentDirectory(currentDirectory.toFile());
}
int ret = fileChooser.showDialog(mainPanel, NLS.str("file.open_title"));
if (ret == JFileChooser.APPROVE_OPTION) {
settings.setLastOpenFilePath(fileChooser.getCurrentDirectory().getPath());
openFile(fileChooser.getSelectedFile());
settings.setLastOpenFilePath(fileChooser.getCurrentDirectory().toPath());
open(fileChooser.getSelectedFile().toPath());
}
}
private void newProject() {
if (!ensureProjectIsSaved()) {
return;
}
project = new JadxProject(settings);
update();
clearTree();
}
public void openFile(File file) {
private void clearTree() {
tabbedPane.closeAllTabs();
resetCache();
wrapper.openFile(file);
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
settings.addRecentFile(file.getAbsolutePath());
initTree();
setTitle(DEFAULT_TITLE + " - " + file.getName());
runBackgroundJobs();
treeRoot = null;
treeModel.setRoot(treeRoot);
treeModel.reload();
}
private void saveProject() {
if (project.getProjectPath() == null) {
saveProjectAs();
}
else {
project.save();
update();
}
}
private void saveProjectAs() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setAcceptAllFileFilterUsed(true);
String[] exts = {JadxProject.PROJECT_EXTENSION};
String description = "supported files: " + Arrays.toString(exts).replace('[', '(').replace(']', ')');
fileChooser.setFileFilter(new FileNameExtensionFilter(description, exts));
fileChooser.setToolTipText(NLS.str("file.save_project"));
Path currentDirectory = settings.getLastSaveProjectPath();
if (currentDirectory != null) {
fileChooser.setCurrentDirectory(currentDirectory.toFile());
}
int ret = fileChooser.showSaveDialog(mainPanel);
if (ret == JFileChooser.APPROVE_OPTION) {
settings.setLastSaveProjectPath(fileChooser.getCurrentDirectory().toPath());
Path path = fileChooser.getSelectedFile().toPath();
if (!path.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(JadxProject.PROJECT_EXTENSION)) {
path = path.resolveSibling(path.getFileName() + "." + JadxProject.PROJECT_EXTENSION);
}
if (Files.exists(path)) {
int res = JOptionPane.showConfirmDialog(
this,
NLS.str("confirm.save_as_message", path.getFileName()),
NLS.str("confirm.save_as_title"),
JOptionPane.YES_NO_OPTION);
if (res == JOptionPane.NO_OPTION) {
return;
}
}
project.saveAs(path);
update();
}
}
void open(Path path) {
if (path.getFileName().toString().toLowerCase(Locale.ROOT)
.endsWith(JadxProject.PROJECT_EXTENSION)) {
openProject(path);
}
else {
project.setFilePath(path);
tabbedPane.closeAllTabs();
resetCache();
wrapper.openFile(path.toFile());
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
initTree();
update();
runBackgroundJobs();
}
}
private boolean ensureProjectIsSaved() {
if (project != null && !project.isSaved() && !project.isInitial()) {
int res = JOptionPane.showConfirmDialog(
this,
NLS.str("confirm.not_saved_message"),
NLS.str("confirm.not_saved_title"),
JOptionPane.YES_NO_CANCEL_OPTION);
if (res == JOptionPane.CANCEL_OPTION) {
return false;
}
if (res == JOptionPane.YES_OPTION) {
project.save();
}
}
return true;
}
private void openProject(Path path) {
if (!ensureProjectIsSaved()) {
return;
}
project = JadxProject.from(path, settings);
if (project == null) {
JOptionPane.showMessageDialog(
this,
NLS.str("msg.project_error"),
NLS.str("msg.project_error_title"),
JOptionPane.INFORMATION_MESSAGE
);
return;
}
update();
settings.addRecentProject(path);
Path filePath = project.getFilePath();
if (filePath == null) {
clearTree();
}
else {
open(filePath);
}
}
private void update() {
newProjectAction.setEnabled(!project.isInitial());
saveProjectAction.setEnabled(!project.isSaved());
Path projectPath = project.getProjectPath();
String pathString;
if (projectPath == null) {
pathString = "";
}
else {
pathString = " [" + projectPath.getParent().toAbsolutePath() + ']';
}
setTitle((project.isSaved() ? "" : '*')
+ project.getName() + pathString + " - " + DEFAULT_TITLE);
}
protected void resetCache() {
......@@ -215,7 +384,7 @@ public class MainWindow extends JFrame {
public void reOpenFile() {
File openedFile = wrapper.getOpenFile();
if (openedFile != null) {
openFile(openedFile);
open(openedFile.toPath());
}
}
......@@ -224,12 +393,12 @@ public class MainWindow extends JFrame {
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fileChooser.setToolTipText(NLS.str("file.save_all_msg"));
String currentDirectory = settings.getLastSaveFilePath();
if (!currentDirectory.isEmpty()) {
fileChooser.setCurrentDirectory(new File(currentDirectory));
Path currentDirectory = settings.getLastSaveFilePath();
if (currentDirectory != null) {
fileChooser.setCurrentDirectory(currentDirectory.toFile());
}
int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select"));
int ret = fileChooser.showSaveDialog(mainPanel);
if (ret == JFileChooser.APPROVE_OPTION) {
JadxArgs decompilerArgs = wrapper.getArgs();
decompilerArgs.setExportAsGradleProject(export);
......@@ -240,7 +409,7 @@ public class MainWindow extends JFrame {
decompilerArgs.setSkipSources(settings.isSkipSources());
decompilerArgs.setSkipResources(settings.isSkipResources());
}
settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().getPath());
settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().toPath());
ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100);
progressMonitor.setMillisToPopup(0);
wrapper.saveAll(fileChooser.getSelectedFile(), progressMonitor);
......@@ -351,12 +520,36 @@ public class MainWindow extends JFrame {
Action openAction = new AbstractAction(NLS.str("file.open_action"), ICON_OPEN) {
@Override
public void actionPerformed(ActionEvent e) {
openFile();
openFileOrProject();
}
};
openAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.open_action"));
openAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_DOWN_MASK));
newProjectAction = new AbstractAction(NLS.str("file.new_project")) {
@Override
public void actionPerformed(ActionEvent e) {
newProject();
}
};
newProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.new_project"));
saveProjectAction = new AbstractAction(NLS.str("file.save_project")) {
@Override
public void actionPerformed(ActionEvent e) {
saveProject();
}
};
saveProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project"));
Action saveProjectAsAction = new AbstractAction(NLS.str("file.save_project_as")) {
@Override
public void actionPerformed(ActionEvent e) {
saveProjectAs();
}
};
saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as"));
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) {
@Override
public void actionPerformed(ActionEvent e) {
......@@ -375,8 +568,8 @@ public class MainWindow extends JFrame {
exportAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.export_gradle"));
exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK));
JMenu recentFiles = new JMenu(NLS.str("menu.recent_files"));
recentFiles.addMenuListener(new RecentFilesMenuListener(recentFiles));
JMenu recentProjects = new JMenu(NLS.str("menu.recent_projects"));
recentProjects.addMenuListener(new RecentProjectsMenuListener(recentProjects));
Action prefsAction = new AbstractAction(NLS.str("menu.preferences"), ICON_PREF) {
@Override
......@@ -491,10 +684,15 @@ public class MainWindow extends JFrame {
JMenu file = new JMenu(NLS.str("menu.file"));
file.setMnemonic(KeyEvent.VK_F);
file.add(openAction);
file.addSeparator();
file.add(newProjectAction);
file.add(saveProjectAction);
file.add(saveProjectAsAction);
file.addSeparator();
file.add(saveAllAction);
file.add(exportAction);
file.addSeparator();
file.add(recentFiles);
file.add(recentProjects);
file.addSeparator();
file.add(prefsAction);
file.addSeparator();
......@@ -694,11 +892,13 @@ public class MainWindow extends JFrame {
tabbedPane.loadSettings();
}
@Override
public void dispose() {
private void closeWindow() {
if (!ensureProjectIsSaved()) {
return;
}
settings.saveWindowPos(this);
cancelBackgroundJobs();
super.dispose();
dispose();
}
public JadxWrapper getWrapper() {
......@@ -721,28 +921,27 @@ public class MainWindow extends JFrame {
return backgroundWorker;
}
private class RecentFilesMenuListener implements MenuListener {
private final JMenu recentFiles;
private class RecentProjectsMenuListener implements MenuListener {
private final JMenu recentProjects;
public RecentFilesMenuListener(JMenu recentFiles) {
this.recentFiles = recentFiles;
public RecentProjectsMenuListener(JMenu recentProjects) {
this.recentProjects = recentProjects;
}
@Override
public void menuSelected(MenuEvent menuEvent) {
recentFiles.removeAll();
recentProjects.removeAll();
File openFile = wrapper.getOpenFile();
String currentFile = openFile == null ? "" : openFile.getAbsolutePath();
for (final String file : settings.getRecentFiles()) {
if (file.equals(currentFile)) {
continue;
Path currentPath = openFile == null ? null : openFile.toPath();
for (final Path path : settings.getRecentProjects()) {
if (!path.equals(currentPath)) {
JMenuItem menuItem = new JMenuItem(path.toAbsolutePath().toString());
recentProjects.add(menuItem);
menuItem.addActionListener(e -> open(path));
}
JMenuItem menuItem = new JMenuItem(file);
recentFiles.add(menuItem);
menuItem.addActionListener(e -> openFile(new File(file)));
}
if (recentFiles.getItemCount() == 0) {
recentFiles.add(new JMenuItem(NLS.str("menu.no_recent_files")));
if (recentProjects.getItemCount() == 0) {
recentProjects.add(new JMenuItem(NLS.str("menu.no_recent_projects")));
}
}
......
......@@ -61,12 +61,14 @@ public class NLS {
i18nMessagesMap.put(locale, bundle);
}
public static String str(String key) {
public static String str(String key, Object... parameters) {
String value;
try {
return localizedMessagesMap.getString(key);
value = localizedMessagesMap.getString(key);
} catch (MissingResourceException e) {
return fallbackMessagesMap.getString(key); // definitely exists
value = fallbackMessagesMap.getString(key); // definitely exists
}
return String.format(value, parameters);
}
public static String str(String key, LangLocale locale) {
......
package jadx.gui.utils;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
public class PathTypeAdapter {
private static TypeAdapter<Path> SINGLETON;
public static TypeAdapter<Path> singleton() {
if (SINGLETON == null) {
SINGLETON = new TypeAdapter<Path>() {
@Override
public void write(JsonWriter out, Path value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(value.toAbsolutePath().toString());
}
}
@Override
public Path read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return Paths.get(in.nextString());
}
};
}
return SINGLETON;
}
private PathTypeAdapter() {
}
}
......@@ -2,8 +2,8 @@ language.name=English
menu.file=File
menu.view=View
menu.recent_files=Recent Files
menu.no_recent_files=No recent files
menu.recent_projects=Recent projects
menu.no_recent_projects=No recent projects
menu.preferences=Preferences
menu.sync=Sync with editor
menu.flatten=Show flatten packages
......@@ -20,17 +20,18 @@ menu.update_label=New version %s available!
file.open_action=Open file...
file.open_title=Open file
file.new_project=New project
file.save_project=Save project
file.save_project_as=Save project as...
file.save_all=Save all
file.export_gradle=Save as gradle project
file.save_all_msg=Select directory for save decompiled sources
file.select=Select
file.exit=Exit
tree.sources_title=Source code
tree.resources_title=Resources
tree.loading=Loading...
search=Search
search.previous=Previous
search.next=Next
search.mark_all=Mark All
......@@ -79,6 +80,7 @@ preferences.title=Preferences
preferences.deobfuscation=Deobfuscation
preferences.editor=Editor
preferences.decompile=Decompilation
preferences.project=Project
preferences.other=Other
preferences.language=Language
preferences.check_for_updates=Check for updates on startup
......@@ -89,6 +91,7 @@ preferences.replaceConsts=Replace constants
preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers
preferences.useImports=Use import statements
preferences.skipResourcesDecode=Don't decode resources
preferences.autoSave=Auto save
preferences.threads=Processing threads count
preferences.excludedPackages=Excluded packages
preferences.excludedPackages.tooltip=List of space separated package names that will not be decompiled or indexed (saves RAM)
......@@ -116,6 +119,8 @@ msg.saving_sources=Saving sources...
msg.language_changed_title=Language changed
msg.language_changed=New language will be displayed the next time application starts.
msg.index_not_initialized=Index not initialized, search will be disabled!
msg.project_error_title=Error
msg.project_error=Project could not be loaded
popup.undo=Undo
popup.redo=Redo
......@@ -127,11 +132,15 @@ popup.select_all=Select All
popup.find_usage=Find Usage
popup.exclude=Exclude
confirm.save_as_title=Confirm Save as
confirm.save_as_message=%s already exists.\nDo you want to replace it?
confirm.not_saved_title=Save project
confirm.not_saved_message=Save the current project before opening the new one?
certificate.title=Certificate
certificate.cert_type=Type
certificate.serialSigVer=Version
certificate.serialNumber=Serial number
certificate.cert_issuer=Issuer
certificate.cert_subject=Subject
certificate.serialValidFrom=Valid from
certificate.serialValidUntil=Valid until
......
......@@ -2,8 +2,8 @@ language.name=Español
menu.file=Archivo
menu.view=Vista
menu.recent_files=Archivos recientes
menu.no_recent_files=No hay archivos recientes
#menu.recent_projects=
#menu.no_recent_projects=
menu.preferences=Preferencias
menu.sync=Sincronizar con el editor
menu.flatten=Mostrar paquetes en vista plana
......@@ -20,17 +20,18 @@ menu.update_label=¡Nueva versión %s disponible!
file.open_action=Abrir archivo...
file.open_title=Abrir archivo
#file.new_project=
#file.save_project=
#file.save_project_as=
file.save_all=Guardar todo
file.export_gradle=Guardar como proyecto Gradle
file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas
file.select=Seleccionar
file.exit=Salir
tree.sources_title=Código fuente
tree.resources_title=Recursos
tree.loading=Cargando...
search=Buscar
search.previous=Anterior
search.next=Siguiente
search.mark_all=Marcar todo
......@@ -79,6 +80,7 @@ preferences.title=Preferencias
preferences.deobfuscation=Desofuscación
preferences.editor=Editor
preferences.decompile=Descompilación
#preferences.project=
preferences.other=Otros
preferences.language=Idioma
preferences.check_for_updates=Buscar actualizaciones al iniciar
......@@ -89,6 +91,7 @@ preferences.replaceConsts=Reemplazar constantes
#preferences.respectBytecodeAccessModifiers=
#preferences.useImports=
preferences.skipResourcesDecode=No descodificar recursos
#preferences.autoSave=
preferences.threads=Número de hilos a procesar
#preferences.excludedPackages=
#preferences.excludedPackages.tooltip=
......@@ -116,6 +119,8 @@ msg.saving_sources=Guardando fuente...
msg.language_changed_title=Idioma cambiado
msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie.
msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivará!
#msg.project_error_title=
#msg.project_error=
popup.undo=Deshacer
popup.redo=Rehacer
......@@ -127,11 +132,15 @@ popup.select_all=Seleccionar todo
#popup.find_usage=
#popup.exclude=
#confirm.save_as_title=
#confirm.save_as_message=
#confirm.not_saved_title=
#confirm.not_saved_message=
certificate.title=Certificado
certificate.cert_type=Tipo
certificate.serialSigVer=Versión
certificate.serialNumber=Número de serial
certificate.cert_issuer=Issuer
certificate.cert_subject=Subject
certificate.serialValidFrom=Válido desde
certificate.serialValidUntil=Válido hasta
......
......@@ -2,8 +2,8 @@ language.name=中文(简体)
menu.file=文件
menu.view=视图
menu.recent_files=最近打开的文件
menu.no_recent_files=无最近打开的文件
#menu.recent_projects=
#menu.no_recent_projects=
menu.preferences=首选项
menu.sync=与编辑器同步
menu.flatten=展开显示代码包
......@@ -20,17 +20,18 @@ menu.update_label=发现新版本 %s!
file.open_action=打开文件...
file.open_title=打开文件
#file.new_project=
#file.save_project=
#file.save_project_as=
file.save_all=全部保存
file.export_gradle=另存为 Gradle 项目
file.save_all_msg=选择反编译资源路径
file.select=选择
file.exit=退出
tree.sources_title=源代码
tree.resources_title=资源文件
tree.loading=稍等...
search=搜索
search.previous=上一个
search.next=下一个
search.mark_all=标记全部
......@@ -79,6 +80,7 @@ preferences.title=首选项
preferences.deobfuscation=反混淆
preferences.editor=编辑器
preferences.decompile=反编译
#preferences.project=
preferences.other=其他
preferences.language=语言
preferences.check_for_updates=启动时检查更新
......@@ -89,6 +91,7 @@ preferences.replaceConsts=替换常量
preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符
preferences.useImports=使用 import 语句
preferences.skipResourcesDecode=不反编译资源文件
#preferences.autoSave=
preferences.threads=并行线程数
preferences.excludedPackages=排除的包
preferences.excludedPackages.tooltip=将不被解压缩或索引的以空格分隔的包名称列表(节省 RAM)
......@@ -116,6 +119,8 @@ msg.saving_sources=正在导出源代码...
msg.language_changed_title=语言已更改
msg.language_changed=在下次启动时将会显示新的语言。
msg.index_not_initialized=索引尚未初始化,无法进行搜索!
#msg.project_error_title=
#msg.project_error=
popup.undo=撤销
popup.redo=重做
......@@ -127,11 +132,15 @@ popup.select_all=全选
popup.find_usage=查找用例
#popup.exclude=
#confirm.save_as_title=
#confirm.save_as_message=
#confirm.not_saved_title=
#confirm.not_saved_message=
certificate.title=证书
certificate.cert_type=类型
certificate.serialSigVer=版本
certificate.serialNumber=序列号
certificate.cert_issuer=颁发者
certificate.cert_subject=主题
certificate.serialValidFrom=有效期始
certificate.serialValidUntil=有效期至
......
package jadx.gui;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class TestI18n {
private static Path guiJavaPath;
private static Path i18nPath;
private List<String> reference;
private String referenceName;
@BeforeAll
public static void init() {
i18nPath = Paths.get("src/main/resources/i18n");
assertTrue(Files.exists(i18nPath));
guiJavaPath = Paths.get("src/main/java");
assertTrue(Files.exists(guiJavaPath));
}
@Test
public void filesExactlyMatch() throws IOException {
Path path = Paths.get("./src/main/resources/i18n");
assertTrue(Files.exists(path));
Files.list(path).forEach(p -> {
Files.list(i18nPath).forEach(p -> {
List<String> lines;
try {
lines = Files.readAllLines(p);
......@@ -45,12 +64,12 @@ public class TestI18n {
if (p0 != -1) {
String prefix = line.substring(0, p0 + 1);
if (i >= lines.size() || !trimComment(lines.get(i)).startsWith(prefix)) {
fail(path, i + 1);
failLine(path, i + 1);
}
}
}
if (lines.size() != reference.size()) {
fail(path, reference.size());
failLine(path, reference.size());
}
}
......@@ -58,7 +77,37 @@ public class TestI18n {
return string.startsWith("#") ? string.substring(1) : string;
}
private void fail(Path path, int line) {
Assertions.fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line);
private void failLine(Path path, int line) {
fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line);
}
@Test
public void keyIsUsed() throws IOException {
Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(i18nPath.resolve("Messages_en_US.properties"))) {
properties.load(reader);
}
Set<String> keys = new HashSet<>();
for (Object key : properties.keySet()) {
keys.add("\"" + key + '"');
}
Files.walk(guiJavaPath).filter(p -> Files.isRegularFile(p)).forEach(p -> {
try {
List<String> lines = Files.readAllLines(p);
for (String line : lines) {
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
if (line.contains(it.next())) {
it.remove();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
assertThat("keys not used", keys, empty());
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册