diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 3149e9f51..6ed547c95 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -14,10 +14,11 @@ dependencies { // jadx-script autocomplete support implementation(project(":jadx-plugins::jadx-script:jadx-script-ide")) + implementation(project(":jadx-plugins::jadx-script:jadx-script-runtime")) implementation 'org.jetbrains.kotlin:kotlin-scripting-common:1.7.20' implementation 'com.fifesoft:autocomplete:3.3.0' - // use ktlint for lint and format jadx scripts + // use KtLint for format and check jadx scripts implementation 'com.pinterest.ktlint:ktlint-core:0.47.1' implementation 'com.pinterest.ktlint:ktlint-ruleset-standard:0.47.1' diff --git a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java index 42ab16f8a..526bcb611 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.cli.LogHelper; +import jadx.gui.logs.LogCollector; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsAdapter; import jadx.gui.ui.ExceptionDialog; @@ -13,7 +14,6 @@ import jadx.gui.ui.MainWindow; import jadx.gui.utils.LafManager; import jadx.gui.utils.NLS; import jadx.gui.utils.SystemInfo; -import jadx.gui.utils.logs.LogCollector; public class JadxGUI { private static final Logger LOG = LoggerFactory.getLogger(JadxGUI.class); diff --git a/jadx-gui/src/main/java/jadx/gui/logs/ILogListener.java b/jadx-gui/src/main/java/jadx/gui/logs/ILogListener.java new file mode 100644 index 000000000..09c7d470d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/ILogListener.java @@ -0,0 +1,7 @@ +package jadx.gui.logs; + +public interface ILogListener { + void onAppend(LogEvent logEvent); + + void onReload(); +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/IssuesListener.java b/jadx-gui/src/main/java/jadx/gui/logs/IssuesListener.java new file mode 100644 index 000000000..cf10b2626 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/IssuesListener.java @@ -0,0 +1,55 @@ +package jadx.gui.logs; + +import javax.swing.SwingUtilities; + +import ch.qos.logback.classic.Level; + +import jadx.gui.ui.panel.IssuesPanel; +import jadx.gui.utils.rx.DebounceUpdate; + +public class IssuesListener implements ILogListener { + private final IssuesPanel issuesPanel; + private final DebounceUpdate updater; + + private int errors = 0; + private int warnings = 0; + + public IssuesListener(IssuesPanel issuesPanel) { + this.issuesPanel = issuesPanel; + this.updater = new DebounceUpdate(500, this::onUpdate); + } + + private void onUpdate() { + SwingUtilities.invokeLater(() -> issuesPanel.onUpdate(errors, warnings)); + } + + @Override + public void onAppend(LogEvent logEvent) { + switch (logEvent.getLevel().toInt()) { + case Level.ERROR_INT: + errors++; + updater.requestUpdate(); + break; + + case Level.WARN_INT: + warnings++; + updater.requestUpdate(); + break; + } + } + + @Override + public void onReload() { + errors = 0; + warnings = 0; + updater.requestUpdate(); + } + + public int getErrors() { + return errors; + } + + public int getWarnings() { + return warnings; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/logs/LimitedQueue.java b/jadx-gui/src/main/java/jadx/gui/logs/LimitedQueue.java similarity index 96% rename from jadx-gui/src/main/java/jadx/gui/utils/logs/LimitedQueue.java rename to jadx-gui/src/main/java/jadx/gui/logs/LimitedQueue.java index 62f6ddf0f..14c074eb2 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/logs/LimitedQueue.java +++ b/jadx-gui/src/main/java/jadx/gui/logs/LimitedQueue.java @@ -1,4 +1,4 @@ -package jadx.gui.utils.logs; +package jadx.gui.logs; import java.util.AbstractQueue; import java.util.ArrayDeque; diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogAppender.java b/jadx-gui/src/main/java/jadx/gui/logs/LogAppender.java new file mode 100644 index 000000000..b981378ab --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogAppender.java @@ -0,0 +1,51 @@ +package jadx.gui.logs; + +import org.apache.commons.lang3.StringUtils; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.utils.UiUtils; + +import static jadx.plugins.script.runtime.JadxScriptTemplateKt.JADX_SCRIPT_LOG_PREFIX; + +class LogAppender implements ILogListener { + private final LogOptions options; + private final RSyntaxTextArea textArea; + + public LogAppender(LogOptions options, RSyntaxTextArea textArea) { + this.options = options; + this.textArea = textArea; + } + + @Override + public void onAppend(LogEvent logEvent) { + if (accept(logEvent)) { + UiUtils.uiRun(() -> textArea.append(logEvent.getMsg())); + } + } + + @Override + public void onReload() { + UiUtils.uiRunAndWait(() -> textArea.append(StringUtils.repeat('=', 100) + '\n')); + } + + private boolean accept(LogEvent logEvent) { + boolean byLevel = logEvent.getLevel().isGreaterOrEqual(options.getLogLevel()); + if (!byLevel) { + return false; + } + switch (options.getMode()) { + case ALL: + return true; + + case ALL_SCRIPTS: + return logEvent.getLoggerName().startsWith(JADX_SCRIPT_LOG_PREFIX); + + case CURRENT_SCRIPT: + return logEvent.getLoggerName().equals(options.getFilter()); + + default: + throw new JadxRuntimeException("Unexpected log mode: " + options.getMode()); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogCollector.java b/jadx-gui/src/main/java/jadx/gui/logs/LogCollector.java new file mode 100644 index 000000000..f06089bfa --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogCollector.java @@ -0,0 +1,83 @@ +package jadx.gui.logs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Layout; + +public class LogCollector extends AppenderBase { + public static final int BUFFER_SIZE = 5000; + + private static final LogCollector INSTANCE = new LogCollector(); + + public static LogCollector getInstance() { + return INSTANCE; + } + + public static void register() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + LoggerContext loggerContext = rootLogger.getLoggerContext(); + + PatternLayout layout = new PatternLayout(); + layout.setContext(loggerContext); + layout.setPattern("%-5level: %msg%n"); + layout.start(); + + INSTANCE.setContext(loggerContext); + INSTANCE.setLayout(layout); + INSTANCE.start(); + + rootLogger.addAppender(INSTANCE); + } + + private final List listeners = new ArrayList<>(); + private final Queue buffer = new LimitedQueue<>(BUFFER_SIZE); + + private Layout layout; + + public LogCollector() { + setName("LogCollector"); + } + + @Override + protected synchronized void append(ILoggingEvent event) { + String msg = layout.doLayout(event); + LogEvent logEvent = new LogEvent(event.getLevel(), event.getLoggerName(), msg); + buffer.offer(logEvent); + listeners.forEach(l -> l.onAppend(logEvent)); + } + + private void setLayout(Layout layout) { + this.layout = layout; + } + + public synchronized void registerListener(ILogListener listener) { + listeners.add(listener); + buffer.forEach(listener::onAppend); + } + + public synchronized boolean removeListener(@Nullable ILogListener listener) { + if (listener == null) { + return false; + } + return this.listeners.removeIf(l -> l == listener); + } + + public synchronized boolean removeListenerByClass(Class listenerCls) { + return this.listeners.removeIf(l -> l.getClass().equals(listenerCls)); + } + + public synchronized void reset() { + buffer.clear(); + listeners.forEach(ILogListener::onReload); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogEvent.java b/jadx-gui/src/main/java/jadx/gui/logs/LogEvent.java new file mode 100644 index 000000000..db55cf5f8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogEvent.java @@ -0,0 +1,32 @@ +package jadx.gui.logs; + +import ch.qos.logback.classic.Level; + +public final class LogEvent { + private final Level level; + private final String loggerName; + private final String msg; + + LogEvent(Level level, String loggerName, String msg) { + this.level = level; + this.loggerName = loggerName; + this.msg = msg; + } + + public Level getLevel() { + return level; + } + + public String getLoggerName() { + return loggerName; + } + + public String getMsg() { + return msg; + } + + @Override + public String toString() { + return level + ": " + loggerName + " - " + msg; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogMode.java b/jadx-gui/src/main/java/jadx/gui/logs/LogMode.java new file mode 100644 index 000000000..496a25e05 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogMode.java @@ -0,0 +1,22 @@ +package jadx.gui.logs; + +import org.apache.commons.lang3.StringUtils; + +import jadx.gui.utils.NLS; + +public enum LogMode { + ALL, + ALL_SCRIPTS, + CURRENT_SCRIPT; + + private static final String[] NLS_STRINGS = StringUtils.split(NLS.str("log_viewer.modes"), '|'); + + public String getLocalizedName() { + return NLS_STRINGS[this.ordinal()]; + } + + @Override + public String toString() { + return getLocalizedName(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogOptions.java b/jadx-gui/src/main/java/jadx/gui/logs/LogOptions.java new file mode 100644 index 000000000..01c570b37 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogOptions.java @@ -0,0 +1,72 @@ +package jadx.gui.logs; + +import org.jetbrains.annotations.Nullable; + +import ch.qos.logback.classic.Level; + +import jadx.core.utils.Utils; + +import static jadx.plugins.script.runtime.JadxScriptTemplateKt.JADX_SCRIPT_LOG_PREFIX; + +public class LogOptions { + + /** + * Store latest requested log options + */ + private static LogOptions current = new LogOptions(LogMode.ALL, Level.INFO, null); + + public static LogOptions allWithLevel(@Nullable Level logLevel) { + Level level = Utils.getOrElse(logLevel, current.getLogLevel()); + return store(new LogOptions(LogMode.ALL, level, null)); + } + + public static LogOptions forLevel(@Nullable Level logLevel) { + Level level = Utils.getOrElse(logLevel, current.getLogLevel()); + return store(new LogOptions(current.getMode(), level, current.getFilter())); + } + + public static LogOptions forMode(LogMode mode) { + return store(new LogOptions(mode, current.getLogLevel(), current.getFilter())); + } + + public static LogOptions forScript(String scriptName) { + String filter = JADX_SCRIPT_LOG_PREFIX + scriptName; + return store(new LogOptions(LogMode.CURRENT_SCRIPT, current.getLogLevel(), filter)); + } + + public static LogOptions current() { + return current; + } + + private static LogOptions store(LogOptions logOptions) { + current = logOptions; + return logOptions; + } + + private final LogMode mode; + private final Level logLevel; + private final @Nullable String filter; + + private LogOptions(LogMode mode, Level logLevel, @Nullable String filter) { + this.mode = mode; + this.logLevel = logLevel; + this.filter = filter; + } + + public LogMode getMode() { + return mode; + } + + public Level getLogLevel() { + return logLevel; + } + + public @Nullable String getFilter() { + return filter; + } + + @Override + public String toString() { + return "LogOptions{mode=" + mode + ", logLevel=" + logLevel + ", filter='" + filter + '\'' + '}'; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/logs/LogPanel.java b/jadx-gui/src/main/java/jadx/gui/logs/LogPanel.java new file mode 100644 index 000000000..290d94a7c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/logs/LogPanel.java @@ -0,0 +1,176 @@ +package jadx.gui.logs; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.event.ChangeListener; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.jetbrains.annotations.Nullable; + +import ch.qos.logback.classic.Level; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.treemodel.JInputScript; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.ui.codearea.AbstractCodeArea; +import jadx.gui.ui.panel.ContentPanel; +import jadx.gui.utils.NLS; + +public class LogPanel extends JPanel { + private static final long serialVersionUID = -8077649118322056081L; + + private static final Level[] LEVEL_ITEMS = { Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR }; + + private final MainWindow mainWindow; + private final Runnable dockAction; + private final Runnable hideAction; + + private RSyntaxTextArea textPane; + private JComboBox modeCb; + private JComboBox levelCb; + + private ChangeListener activeTabListener; + + public LogPanel(MainWindow mainWindow, LogOptions logOptions, Runnable dockAction, Runnable hideAction) { + this.mainWindow = mainWindow; + this.dockAction = dockAction; + this.hideAction = hideAction; + initUI(logOptions); + applyLogOptions(logOptions); + } + + public void applyLogOptions(LogOptions logOptions) { + if (logOptions.getMode() == LogMode.CURRENT_SCRIPT) { + String scriptName = getCurrentScriptName(); + if (scriptName != null) { + logOptions = LogOptions.forScript(scriptName); + } + registerActiveTabListener(); + } else { + removeActiveTabListener(); + } + if (modeCb.getSelectedItem() != logOptions.getMode()) { + modeCb.setSelectedItem(logOptions.getMode()); + } + if (levelCb.getSelectedItem() != logOptions.getLogLevel()) { + levelCb.setSelectedItem(logOptions.getLogLevel()); + } + registerLogListener(logOptions); + } + + public void loadSettings() { + AbstractCodeArea.loadCommonSettings(mainWindow, textPane); + } + + private void initUI(LogOptions logOptions) { + JadxSettings settings = mainWindow.getSettings(); + textPane = AbstractCodeArea.getDefaultArea(mainWindow); + textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + modeCb = new JComboBox<>(LogMode.values()); + modeCb.setSelectedItem(logOptions.getMode()); + modeCb.addActionListener(e -> applyLogOptions(LogOptions.forMode((LogMode) modeCb.getSelectedItem()))); + JLabel modeLabel = new JLabel(NLS.str("log_viewer.mode")); + modeLabel.setLabelFor(modeCb); + + levelCb = new JComboBox<>(LEVEL_ITEMS); + levelCb.setSelectedItem(logOptions.getLogLevel()); + levelCb.addActionListener(e -> applyLogOptions(LogOptions.forLevel((Level) levelCb.getSelectedItem()))); + JLabel levelLabel = new JLabel(NLS.str("log_viewer.log_level")); + levelLabel.setLabelFor(levelCb); + + JButton clearBtn = new JButton(NLS.str("log_viewer.clear")); + clearBtn.addActionListener(ev -> { + LogCollector.getInstance().reset(); + textPane.setText(""); + }); + + JButton dockBtn = new JButton(NLS.str(settings.isDockLogViewer() ? "log_viewer.undock" : "log_viewer.dock")); + dockBtn.addActionListener(ev -> dockAction.run()); + + JButton hideBtn = new JButton(NLS.str("log_viewer.hide")); + hideBtn.addActionListener(ev -> hideAction.run()); + + JPanel start = new JPanel(); + start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS)); + start.add(modeLabel); + start.add(Box.createRigidArea(new Dimension(5, 0))); + start.add(modeCb); + start.add(Box.createRigidArea(new Dimension(15, 0))); + start.add(levelLabel); + start.add(Box.createRigidArea(new Dimension(5, 0))); + start.add(levelCb); + start.add(Box.createRigidArea(new Dimension(5, 0))); + + JPanel end = new JPanel(); + end.setLayout(new BoxLayout(end, BoxLayout.LINE_AXIS)); + end.add(clearBtn); + end.add(Box.createRigidArea(new Dimension(15, 0))); + end.add(dockBtn); + end.add(Box.createRigidArea(new Dimension(15, 0))); + end.add(hideBtn); + + JPanel controlPane = new JPanel(); + controlPane.setLayout(new BorderLayout()); + controlPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + controlPane.add(start, BorderLayout.LINE_START); + controlPane.add(end, BorderLayout.LINE_END); + + JScrollPane scrollPane = new JScrollPane(textPane); + + setLayout(new BorderLayout(5, 5)); + add(controlPane, BorderLayout.PAGE_START); + add(scrollPane, BorderLayout.CENTER); + } + + private void registerLogListener(LogOptions logOptions) { + LogCollector logCollector = LogCollector.getInstance(); + logCollector.removeListenerByClass(LogAppender.class); + textPane.setText(""); + logCollector.registerListener(new LogAppender(logOptions, textPane)); + } + + private @Nullable String getCurrentScriptName() { + ContentPanel selectedCodePanel = mainWindow.getTabbedPane().getSelectedCodePanel(); + if (selectedCodePanel != null) { + JNode node = selectedCodePanel.getNode(); + if (node instanceof JInputScript) { + return node.getName(); + } + } + return null; + } + + private synchronized void registerActiveTabListener() { + removeActiveTabListener(); + activeTabListener = e -> { + String scriptName = getCurrentScriptName(); + if (scriptName != null) { + applyLogOptions(LogOptions.forScript(scriptName)); + } + }; + mainWindow.getTabbedPane().addChangeListener(activeTabListener); + } + + private synchronized void removeActiveTabListener() { + if (activeTabListener != null) { + mainWindow.getTabbedPane().removeChangeListener(activeTabListener); + activeTabListener = null; + } + } + + public void dispose() { + LogCollector.getInstance().removeListenerByClass(LogAppender.class); + removeActiveTabListener(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java index c2316aa73..c3b94923a 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -22,8 +22,10 @@ import org.slf4j.LoggerFactory; import com.pinterest.ktlint.core.LintError; import kotlin.script.experimental.api.ScriptDiagnostic; +import kotlin.script.experimental.api.ScriptDiagnostic.Severity; import jadx.gui.JadxWrapper; +import jadx.gui.logs.LogOptions; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.JInputScript; @@ -40,17 +42,18 @@ import jadx.gui.utils.ui.NodeLabel; import jadx.plugins.script.ide.ScriptAnalyzeResult; import jadx.plugins.script.ide.ScriptCompiler; +import static jadx.plugins.script.runtime.JadxScriptTemplateKt.JADX_SCRIPT_LOG_PREFIX; + public class ScriptContentPanel extends AbstractCodeContentPanel { private static final long serialVersionUID = 6575696321112417513L; - private static final Logger LOG = LoggerFactory.getLogger(ScriptContentPanel.class); - private final ScriptCodeArea scriptArea; private final SearchBar searchBar; private final RTextScrollPane codeScrollPane; private final JPanel actionPanel; private final JLabel resultLabel; private final ScriptErrorService errorService; + private final Logger scriptLog; public ScriptContentPanel(TabbedPane panel, JInputScript scriptNode) { super(panel, scriptNode); @@ -60,6 +63,7 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { actionPanel = buildScriptActionsPanel(); searchBar = new SearchBar(scriptArea); codeScrollPane = new RTextScrollPane(scriptArea); + scriptLog = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.getName()); initUI(); applySettings(); @@ -136,7 +140,7 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { wrapper.resetGuiPluginsContext(); wrapper.getDecompiler().reloadPasses(); } catch (Exception e) { - LOG.error("Passes reload failed", e); + scriptLog.error("Passes reload failed", e); } }, taskStatus -> { tabbedPane.reloadInactiveTabs(); @@ -153,12 +157,16 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { ScriptCompiler scriptCompiler = new ScriptCompiler(fileName); ScriptAnalyzeResult result = scriptCompiler.analyze(code, scriptArea.getCaretPosition()); List issues = result.getIssues(); + boolean success = true; for (ScriptDiagnostic issue : issues) { - LOG.warn("Compiler issue: {}", issue); + Severity severity = issue.getSeverity(); + if (severity == Severity.ERROR || severity == Severity.FATAL) { + scriptLog.error("{}", issue.render(false, true, true, true)); + success = false; + } else { + scriptLog.warn("Compiler issue: {}", issue); + } } - boolean success = issues.stream().map(ScriptDiagnostic::getSeverity) - .noneMatch(s -> s == ScriptDiagnostic.Severity.ERROR || s == ScriptDiagnostic.Severity.FATAL); - List lintErrs = Collections.emptyList(); if (success) { lintErrs = getLintIssues(code, fileName); @@ -170,12 +178,13 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { errorService.apply(); if (!success) { resultLabel.setText("Compiler issues: " + issues.size()); + getTabbedPane().getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName())); } else if (!lintErrs.isEmpty()) { resultLabel.setText("Lint issues: " + lintErrs.size()); } return success; } catch (Throwable e) { - LOG.error("Failed to check code", e); + scriptLog.error("Failed to check code", e); return true; } } @@ -184,11 +193,12 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { try { List lintErrs = KtLintUtils.INSTANCE.lint(code, fileName); for (LintError error : lintErrs) { - LOG.warn("Lint issue: {}", error); + scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})", + error.getDetail(), error.getLine(), error.getCol(), error.getRuleId()); } return lintErrs; } catch (Throwable e) { // can throw initialization error - LOG.warn("KtLint failed", e); + scriptLog.warn("KtLint failed", e); return Collections.emptyList(); } } @@ -205,7 +215,7 @@ public class ScriptContentPanel extends AbstractCodeContentPanel { errorService.clearErrors(); } } catch (Throwable e) { // can throw initialization error - LOG.error("Failed to reformat code", e); + scriptLog.error("Failed to reformat code", e); } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 57f92521c..84196dcd4 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -105,6 +105,8 @@ public class JadxSettings extends JadxCLIArgs { */ private int treeWidth = 130; + private boolean dockLogViewer = true; + private int settingsVersion = 0; @JadxSettingsAdapter.GsonExclude @@ -686,6 +688,15 @@ public class JadxSettings extends JadxCLIArgs { this.jumpOnDoubleClick = jumpOnDoubleClick; } + public boolean isDockLogViewer() { + return dockLogViewer; + } + + public void setDockLogViewer(boolean dockLogViewer) { + this.dockLogViewer = dockLogViewer; + partialSync(settings -> this.dockLogViewer = dockLogViewer); + } + private void upgradeSettings(int fromVersion) { LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion == 0) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 6cc501019..d389c7337 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -33,15 +33,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.AbstractAction; @@ -53,7 +50,6 @@ 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; @@ -65,8 +61,6 @@ import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; 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.tree.DefaultMutableTreeNode; @@ -105,6 +99,9 @@ import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.DecompileTask; import jadx.gui.jobs.ExportTask; import jadx.gui.jobs.TaskStatus; +import jadx.gui.logs.LogCollector; +import jadx.gui.logs.LogOptions; +import jadx.gui.logs.LogPanel; import jadx.gui.plugins.quark.QuarkDialog; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; @@ -130,6 +127,7 @@ import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.IssuesPanel; import jadx.gui.ui.panel.JDebuggerPanel; import jadx.gui.ui.panel.ProgressPanel; +import jadx.gui.ui.popupmenu.RecentProjectsMenuListener; import jadx.gui.ui.treenodes.StartPageNode; import jadx.gui.ui.treenodes.SummaryNode; import jadx.gui.update.JadxUpdate; @@ -145,7 +143,6 @@ import jadx.gui.utils.NLS; import jadx.gui.utils.SystemInfo; import jadx.gui.utils.UiUtils; import jadx.gui.utils.fileswatcher.LiveReloadWorker; -import jadx.gui.utils.logs.LogCollector; import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.NodeLabel; import jadx.plugins.mappings.save.MappingExporter; @@ -197,8 +194,10 @@ public class MainWindow extends JFrame { private MappingFormat currentMappingFormat; private boolean renamesChanged = false; - private JPanel mainPanel; - private JSplitPane splitPane; + private transient JPanel mainPanel; + private transient JSplitPane treeSplitPane; + private transient JSplitPane rightSplitPane; + private transient JSplitPane bottomSplitPane; private JTree tree; private DefaultTreeModel treeModel; @@ -221,8 +220,9 @@ public class MainWindow extends JFrame { private transient ProgressPanel progressPane; private transient Theme editorTheme; - private JDebuggerPanel debuggerPanel; - private JSplitPane verticalSplitter; + private transient IssuesPanel issuesPanel; + private transient @Nullable LogPanel logPanel; + private transient @Nullable JDebuggerPanel debuggerPanel; private final List loadListeners = new ArrayList<>(); private boolean loaded; @@ -238,6 +238,7 @@ public class MainWindow extends JFrame { resetCache(); FontUtils.registerBundledFonts(); + setEditorTheme(settings.getEditorThemePath()); initUI(); this.backgroundExecutor = new BackgroundExecutor(settings, progressPane); initMenuAndToolbar(); @@ -252,7 +253,7 @@ public class MainWindow extends JFrame { public void init() { pack(); setLocationAndPosition(); - splitPane.setDividerLocation(settings.getTreeWidth()); + treeSplitPane.setDividerLocation(settings.getTreeWidth()); heapUsageBar.setVisible(settings.isShowHeapUsageBar()); setVisible(true); setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); @@ -634,7 +635,7 @@ public class MainWindow extends JFrame { if (!wrapper.getClasses().isEmpty()) { return; } - int errors = LogCollector.getInstance().getErrors(); + int errors = issuesPanel.getErrorsCount(); if (errors > 0) { int result = JOptionPane.showConfirmDialog(this, NLS.str("message.load_errors", errors), @@ -642,7 +643,7 @@ public class MainWindow extends JFrame { JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE); if (result == JOptionPane.OK_OPTION) { - LogViewerDialog.openWithLevel(this, Level.ERROR); + showLogViewer(LogOptions.allWithLevel(Level.ERROR)); } } else { UiUtils.showMessageBox(this, NLS.str("message.no_classes")); @@ -1101,7 +1102,7 @@ public class MainWindow extends JFrame { exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); JMenu recentProjects = new JMenu(NLS.str("menu.recent_projects")); - recentProjects.addMenuListener(new RecentProjectsMenuListener(recentProjects)); + recentProjects.addMenuListener(new RecentProjectsMenuListener(this, recentProjects)); Action prefsAction = new AbstractAction(NLS.str("menu.preferences"), ICON_PREF) { @Override @@ -1140,6 +1141,10 @@ public class MainWindow extends JFrame { } }); + JCheckBoxMenuItem dockLog = new JCheckBoxMenuItem(NLS.str("menu.dock_log")); + dockLog.setState(settings.isDockLogViewer()); + dockLog.addActionListener(event -> settings.setDockLogViewer(!settings.isDockLogViewer())); + Action syncAction = new AbstractAction(NLS.str("menu.sync"), ICON_SYNC) { @Override public void actionPerformed(ActionEvent e) { @@ -1211,15 +1216,10 @@ public class MainWindow extends JFrame { deobfMenuItem = new JCheckBoxMenuItem(deobfAction); deobfMenuItem.setState(settings.isDeobfuscationOn()); - Action logAction = new AbstractAction(NLS.str("menu.log"), ICON_LOG) { - @Override - public void actionPerformed(ActionEvent e) { - LogViewerDialog.open(MainWindow.this); - } - }; - logAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.log")); - logAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_L, - UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); + ActionHandler showLog = new ActionHandler(ev -> showLogViewer(LogOptions.current())); + showLog.setNameAndDesc(NLS.str("menu.log")); + showLog.setIcon(ICON_LOG); + showLog.setKeyBinding(getKeyStroke(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); Action aboutAction = new AbstractAction(NLS.str("menu.about"), ICON_INFO) { @Override @@ -1296,6 +1296,7 @@ public class MainWindow extends JFrame { view.add(syncAction); view.add(heapUsageBarMenuItem); view.add(alwaysSelectOpened); + view.add(dockLog); JMenu nav = new JMenu(NLS.str("menu.navigation")); nav.setMnemonic(KeyEvent.VK_N); @@ -1319,7 +1320,7 @@ public class MainWindow extends JFrame { JMenu help = new JMenu(NLS.str("menu.help")); help.setMnemonic(KeyEvent.VK_H); - help.add(logAction); + help.add(showLog); if (Jadx.isDevVersion()) { help.add(new AbstractAction("Show sample error report") { @Override @@ -1373,7 +1374,7 @@ public class MainWindow extends JFrame { toolbar.add(quarkAction); toolbar.add(openDeviceAction); toolbar.addSeparator(); - toolbar.add(logAction); + toolbar.add(showLog); toolbar.addSeparator(); toolbar.add(prefsAction); toolbar.addSeparator(); @@ -1403,9 +1404,9 @@ public class MainWindow extends JFrame { private void initUI() { setMinimumSize(new Dimension(200, 150)); mainPanel = new JPanel(new BorderLayout()); - splitPane = new JSplitPane(); - splitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); - mainPanel.add(splitPane); + treeSplitPane = new JSplitPane(); + treeSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); + mainPanel.add(treeSplitPane); DefaultMutableTreeNode treeRootNode = new DefaultMutableTreeNode(NLS.str("msg.open_file")); treeModel = new DefaultTreeModel(treeRootNode); @@ -1486,7 +1487,7 @@ public class MainWindow extends JFrame { }); progressPane = new ProgressPanel(this, true); - IssuesPanel issuesPanel = new IssuesPanel(this); + issuesPanel = new IssuesPanel(this); JPanel leftPane = new JPanel(new BorderLayout()); JScrollPane treeScrollPane = new JScrollPane(tree); @@ -1498,22 +1499,27 @@ public class MainWindow extends JFrame { leftPane.add(treeScrollPane, BorderLayout.CENTER); leftPane.add(bottomPane, BorderLayout.PAGE_END); - splitPane.setLeftComponent(leftPane); + treeSplitPane.setLeftComponent(leftPane); tabbedPane = new TabbedPane(this); tabbedPane.setMinimumSize(new Dimension(150, 150)); - splitPane.setRightComponent(tabbedPane); + + rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + rightSplitPane.setTopComponent(tabbedPane); + rightSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); + + treeSplitPane.setRightComponent(rightSplitPane); new DropTarget(this, DnDConstants.ACTION_COPY, new MainDropTarget(this)); heapUsageBar = new HeapUsageBar(); mainPanel.add(heapUsageBar, BorderLayout.SOUTH); - verticalSplitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - verticalSplitter.setTopComponent(splitPane); - verticalSplitter.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); + bottomSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + bottomSplitPane.setTopComponent(treeSplitPane); + bottomSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); - mainPanel.add(verticalSplitter, BorderLayout.CENTER); + mainPanel.add(bottomSplitPane, BorderLayout.CENTER); setContentPane(mainPanel); setTitle(DEFAULT_TITLE); } @@ -1643,6 +1649,9 @@ public class MainWindow extends JFrame { tree.setRowHeight(-1); tabbedPane.loadSettings(); + if (logPanel != null) { + logPanel.loadSettings(); + } } private void closeWindow() { @@ -1650,7 +1659,7 @@ public class MainWindow extends JFrame { if (!ensureProjectIsSaved()) { return; } - settings.setTreeWidth(splitPane.getDividerLocation()); + settings.setTreeWidth(treeSplitPane.getDividerLocation()); settings.saveWindowPos(this); settings.setMainWindowExtendedState(getExtendedState()); if (debuggerPanel != null) { @@ -1696,7 +1705,7 @@ public class MainWindow extends JFrame { } private void saveSplittersInfo() { - settings.setMainWindowVerticalSplitterLoc(verticalSplitter.getDividerLocation()); + settings.setMainWindowVerticalSplitterLoc(bottomSplitPane.getDividerLocation()); settings.setDebuggerStackFrameSplitterLoc(debuggerPanel.getLeftSplitterLocation()); settings.setDebuggerVarTreeSplitterLoc(debuggerPanel.getRightSplitterLocation()); } @@ -1764,49 +1773,44 @@ public class MainWindow extends JFrame { if (debuggerPanel == null) { debuggerPanel = new JDebuggerPanel(this); debuggerPanel.loadSettings(); - verticalSplitter.setBottomComponent(debuggerPanel); + bottomSplitPane.setBottomComponent(debuggerPanel); int loc = settings.getMainWindowVerticalSplitterLoc(); if (loc == 0) { loc = 300; } - verticalSplitter.setDividerLocation(loc); + bottomSplitPane.setDividerLocation(loc); } } - private class RecentProjectsMenuListener implements MenuListener { - private final JMenu menu; - - public RecentProjectsMenuListener(JMenu menu) { - this.menu = menu; + public void showLogViewer(LogOptions logOptions) { + if (settings.isDockLogViewer()) { + showDockedLog(logOptions); + } else { + LogViewerDialog.open(this, logOptions); } + } - @Override - public void menuSelected(MenuEvent menuEvent) { - Set current = new HashSet<>(project.getFilePaths()); - List items = settings.getRecentProjects() - .stream() - .filter(path -> !current.contains(path)) - .map(path -> { - JMenuItem menuItem = new JMenuItem(path.toAbsolutePath().toString()); - menuItem.addActionListener(e -> open(Collections.singletonList(path))); - return menuItem; - }).collect(Collectors.toList()); - - menu.removeAll(); - if (items.isEmpty()) { - menu.add(new JMenuItem(NLS.str("menu.no_recent_projects"))); - } else { - items.forEach(menu::add); - } + private void showDockedLog(LogOptions logOptions) { + if (logPanel != null) { + logPanel.applyLogOptions(logOptions); + return; } + Runnable undock = () -> { + hideDockedLog(); + settings.setDockLogViewer(false); + LogViewerDialog.open(this, logOptions); + }; + logPanel = new LogPanel(this, logOptions, undock, this::hideDockedLog); + rightSplitPane.setBottomComponent(logPanel); + } - @Override - public void menuDeselected(MenuEvent e) { - } - - @Override - public void menuCanceled(MenuEvent e) { + private void hideDockedLog() { + if (logPanel == null) { + return; } + logPanel.dispose(); + logPanel = null; + rightSplitPane.setBottomComponent(null); } public JMenu getPluginsMenu() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java index 32ae9e6ce..0c2112b3e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java @@ -50,6 +50,7 @@ import ch.qos.logback.classic.Level; import jadx.api.JavaClass; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.gui.logs.LogOptions; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; @@ -305,7 +306,7 @@ public abstract class CommonSearchDialog extends JFrame { progressInfoLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - LogViewerDialog.openWithLevel(mainWindow, Level.INFO); + mainWindow.showLogViewer(LogOptions.allWithLevel(Level.INFO)); } }); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/LogViewerDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/LogViewerDialog.java index 65a970fae..5fef29044 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/LogViewerDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/LogViewerDialog.java @@ -5,61 +5,53 @@ import java.awt.Container; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JComboBox; import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; - -import ch.qos.logback.classic.Level; +import jadx.gui.logs.LogOptions; +import jadx.gui.logs.LogPanel; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; -import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.logs.ILogListener; -import jadx.gui.utils.logs.LogCollector; public class LogViewerDialog extends JFrame { private static final long serialVersionUID = -2188700277429054641L; - private static final Level[] LEVEL_ITEMS = { Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR }; - - private static Level level = Level.WARN; - - private final transient JadxSettings settings; - private transient RSyntaxTextArea textPane; - private JComboBox levelCb; private static LogViewerDialog openLogDialog; - public static void open(MainWindow mainWindow) { - openWithLevel(mainWindow, level); - } + private final transient JadxSettings settings; + private final transient LogPanel logPanel; - public static void openWithLevel(MainWindow mainWindow, Level newLevel) { - level = newLevel; - if (openLogDialog == null) { - LogViewerDialog newLogDialog = new LogViewerDialog(mainWindow); - newLogDialog.setVisible(true); - openLogDialog = newLogDialog; + public static void open(MainWindow mainWindow, LogOptions logOptions) { + LogViewerDialog logDialog; + if (openLogDialog != null) { + logDialog = openLogDialog; } else { - LogViewerDialog logDialog = openLogDialog; - logDialog.levelCb.setSelectedItem(level); - logDialog.setVisible(true); - logDialog.toFront(); + logDialog = new LogViewerDialog(mainWindow, logOptions); + openLogDialog = logDialog; } + logDialog.setVisible(true); + logDialog.toFront(); } - private LogViewerDialog(MainWindow mainWindow) { - this.settings = mainWindow.getSettings(); - initUI(mainWindow); - registerLogListener(); + private LogViewerDialog(MainWindow mainWindow, LogOptions logOptions) { + settings = mainWindow.getSettings(); + UiUtils.setWindowIcons(this); + + Runnable dock = () -> { + mainWindow.getSettings().setDockLogViewer(true); + dispose(); + mainWindow.showLogViewer(LogOptions.current()); + }; + logPanel = new LogPanel(mainWindow, logOptions, dock, this::dispose); + Container contentPane = getContentPane(); + contentPane.add(logPanel, BorderLayout.CENTER); + + setTitle(NLS.str("log_viewer.title")); + pack(); + setSize(800, 600); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setLocationRelativeTo(null); settings.loadWindowPos(this); addWindowListener(new WindowAdapter() { @Override @@ -69,68 +61,9 @@ public class LogViewerDialog extends JFrame { }); } - public final void initUI(MainWindow mainWindow) { - UiUtils.setWindowIcons(this); - - textPane = AbstractCodeArea.getDefaultArea(mainWindow); - textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); - - JPanel controlPane = new JPanel(); - controlPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - levelCb = new JComboBox<>(LEVEL_ITEMS); - levelCb.setSelectedItem(level); - levelCb.addActionListener(e -> { - int i = levelCb.getSelectedIndex(); - level = LEVEL_ITEMS[i]; - registerLogListener(); - }); - JLabel levelLabel = new JLabel(NLS.str("log_viewer.log_level")); - levelLabel.setLabelFor(levelCb); - controlPane.add(levelLabel); - controlPane.add(levelCb); - - JScrollPane scrollPane = new JScrollPane(textPane); - - JButton close = new JButton(NLS.str("tabs.close")); - close.addActionListener(event -> close()); - close.setAlignmentX(0.5f); - - Container contentPane = getContentPane(); - contentPane.add(controlPane, BorderLayout.PAGE_START); - contentPane.add(scrollPane, BorderLayout.CENTER); - contentPane.add(close, BorderLayout.PAGE_END); - - setTitle(NLS.str("log_viewer.title")); - pack(); - setSize(800, 600); - setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setLocationRelativeTo(null); - } - - private void registerLogListener() { - LogCollector logCollector = LogCollector.getInstance(); - logCollector.resetListener(); - textPane.setText(""); - logCollector.registerListener(new ILogListener() { - @Override - public Level getFilterLevel() { - return level; - } - - @Override - public void onAppend(final String logStr) { - SwingUtilities.invokeLater(() -> textPane.append(logStr)); - } - }); - } - - private void close() { - dispose(); - } - @Override public void dispose() { - LogCollector.getInstance().resetListener(); + logPanel.dispose(); settings.saveWindowPos(this); super.dispose(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/IssuesPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/IssuesPanel.java index 701512fb6..87d430435 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/IssuesPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/IssuesPanel.java @@ -9,15 +9,15 @@ import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingUtilities; import ch.qos.logback.classic.Level; +import jadx.gui.logs.IssuesListener; +import jadx.gui.logs.LogCollector; +import jadx.gui.logs.LogOptions; import jadx.gui.ui.MainWindow; -import jadx.gui.ui.dialog.LogViewerDialog; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.logs.LogCollector; public class IssuesPanel extends JPanel { private static final long serialVersionUID = -7720576036668459218L; @@ -26,15 +26,19 @@ public class IssuesPanel extends JPanel { private static final ImageIcon WARN_ICON = UiUtils.openSvgIcon("ui/warning"); private final MainWindow mainWindow; + private final IssuesListener issuesListener; private JLabel errorLabel; private JLabel warnLabel; public IssuesPanel(MainWindow mainWindow) { this.mainWindow = mainWindow; initUI(); - LogCollector.getInstance().registerIssueListener((error, warnings) -> { - SwingUtilities.invokeLater(() -> onUpdate(error, warnings)); - }); + this.issuesListener = new IssuesListener(this); + LogCollector.getInstance().registerListener(issuesListener); + } + + public int getErrorsCount() { + return issuesListener.getErrors(); } private void initUI() { @@ -49,13 +53,13 @@ public class IssuesPanel extends JPanel { errorLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - LogViewerDialog.openWithLevel(mainWindow, Level.ERROR); + mainWindow.showLogViewer(LogOptions.allWithLevel(Level.ERROR)); } }); warnLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - LogViewerDialog.openWithLevel(mainWindow, Level.WARN); + mainWindow.showLogViewer(LogOptions.allWithLevel(Level.WARN)); } }); @@ -69,7 +73,7 @@ public class IssuesPanel extends JPanel { add(warnLabel); } - private void onUpdate(int error, int warnings) { + public void onUpdate(int error, int warnings) { if (error == 0 && warnings == 0) { setVisible(false); return; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/RecentProjectsMenuListener.java b/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/RecentProjectsMenuListener.java new file mode 100644 index 000000000..00efdb143 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/RecentProjectsMenuListener.java @@ -0,0 +1,54 @@ +package jadx.gui.ui.popupmenu; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; + +public class RecentProjectsMenuListener implements MenuListener { + private final MainWindow mainWindow; + private final JMenu menu; + + public RecentProjectsMenuListener(MainWindow mainWindow, JMenu menu) { + this.mainWindow = mainWindow; + this.menu = menu; + } + + @Override + public void menuSelected(MenuEvent menuEvent) { + Set current = new HashSet<>(mainWindow.getProject().getFilePaths()); + List items = mainWindow.getSettings().getRecentProjects() + .stream() + .filter(path -> !current.contains(path)) + .map(path -> { + JMenuItem menuItem = new JMenuItem(path.toAbsolutePath().toString()); + menuItem.addActionListener(e -> mainWindow.open(Collections.singletonList(path))); + return menuItem; + }).collect(Collectors.toList()); + + menu.removeAll(); + if (items.isEmpty()) { + menu.add(new JMenuItem(NLS.str("menu.no_recent_projects"))); + } else { + items.forEach(menu::add); + } + } + + @Override + public void menuDeselected(MenuEvent e) { + } + + @Override + public void menuCanceled(MenuEvent e) { + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogIssuesListener.java b/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogIssuesListener.java deleted file mode 100644 index a80829cf1..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogIssuesListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package jadx.gui.utils.logs; - -public interface ILogIssuesListener { - void onChange(int error, int warnings); -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogListener.java b/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogListener.java deleted file mode 100644 index c4dd8c825..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/logs/ILogListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package jadx.gui.utils.logs; - -import ch.qos.logback.classic.Level; - -public interface ILogListener { - Level getFilterLevel(); - - void onAppend(String logStr); -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/logs/LogCollector.java b/jadx-gui/src/main/java/jadx/gui/utils/logs/LogCollector.java deleted file mode 100644 index c1fa25df8..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/logs/LogCollector.java +++ /dev/null @@ -1,132 +0,0 @@ -package jadx.gui.utils.logs; - -import java.util.Queue; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.LoggerFactory; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.PatternLayout; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; -import ch.qos.logback.core.Layout; - -public class LogCollector extends AppenderBase { - public static final int BUFFER_SIZE = 5000; - - private static final LogCollector INSTANCE = new LogCollector(); - - public static LogCollector getInstance() { - return INSTANCE; - } - - public static void register() { - Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - LoggerContext loggerContext = rootLogger.getLoggerContext(); - - PatternLayout layout = new PatternLayout(); - layout.setContext(loggerContext); - layout.setPattern("%-5level: %msg%n"); - layout.start(); - - INSTANCE.setContext(loggerContext); - INSTANCE.setLayout(layout); - INSTANCE.start(); - - rootLogger.addAppender(INSTANCE); - } - - private Layout layout; - - @Nullable - private ILogListener listener; - @Nullable - private ILogIssuesListener issuesListener; - private int errors = 0; - private int warnings = 0; - - private final Queue buffer = new LimitedQueue<>(BUFFER_SIZE); - - public LogCollector() { - setName("LogCollector"); - } - - @Override - protected void append(ILoggingEvent event) { - synchronized (this) { - Level level = event.getLevel(); - String msg = layout.doLayout(event); - store(level, msg); - if (listener != null && level.isGreaterOrEqual(listener.getFilterLevel())) { - listener.onAppend(msg); - } - if (level == Level.ERROR) { - errors++; - issuesUpdated(); - } else if (level == Level.WARN) { - warnings++; - issuesUpdated(); - } - } - } - - private void issuesUpdated() { - if (issuesListener != null) { - issuesListener.onChange(errors, warnings); - } - } - - private void store(Level level, String msg) { - buffer.offer(new LogEvent(level, msg)); - } - - public void setLayout(Layout layout) { - this.layout = layout; - } - - public void registerListener(@NotNull ILogListener listener) { - this.listener = listener; - synchronized (this) { - listener.onAppend(init(listener.getFilterLevel())); - } - } - - public void registerIssueListener(@NotNull ILogIssuesListener listener) { - this.issuesListener = listener; - synchronized (this) { - listener.onChange(errors, warnings); - } - } - - public void resetListener() { - this.listener = null; - } - - public void reset() { - buffer.clear(); - errors = 0; - warnings = 0; - issuesUpdated(); - } - - public int getErrors() { - return errors; - } - - public int getWarnings() { - return warnings; - } - - private String init(Level filterLevel) { - StringBuilder sb = new StringBuilder(); - for (LogEvent event : buffer) { - if (event.getLevel().isGreaterOrEqual(filterLevel)) { - sb.append(event.getMsg()); - } - } - return sb.toString(); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/logs/LogEvent.java b/jadx-gui/src/main/java/jadx/gui/utils/logs/LogEvent.java deleted file mode 100644 index 44b591c90..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/logs/LogEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -package jadx.gui.utils.logs; - -import ch.qos.logback.classic.Level; - -final class LogEvent { - private final Level level; - private final String msg; - - LogEvent(Level level, String msg) { - this.level = level; - this.msg = msg; - } - - public Level getLevel() { - return level; - } - - public String getMsg() { - return msg; - } - - @Override - public String toString() { - return level + ": " + msg; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/rx/DebounceUpdate.java b/jadx-gui/src/main/java/jadx/gui/utils/rx/DebounceUpdate.java new file mode 100644 index 000000000..7cabe4187 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/rx/DebounceUpdate.java @@ -0,0 +1,30 @@ +package jadx.gui.utils.rx; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.FlowableEmitter; +import io.reactivex.FlowableOnSubscribe; +import io.reactivex.disposables.Disposable; + +public class DebounceUpdate { + + private FlowableEmitter emitter; + private final Disposable disposable; + + public DebounceUpdate(int timeMs, Runnable action) { + FlowableOnSubscribe source = emitter -> this.emitter = emitter; + disposable = Flowable.create(source, BackpressureStrategy.LATEST) + .debounce(timeMs, TimeUnit.MILLISECONDS) + .subscribe(v -> action.run()); + } + + public void requestUpdate() { + emitter.onNext(Boolean.TRUE); + } + + public void dispose() { + disposable.dispose(); + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 6173de5fe..25a738055 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -9,6 +9,7 @@ menu.sync=Mit Editor synchronisieren menu.flatten=Codepaket erweitern menu.heapUsageBar=Speicherverbrauchsleiste anzeigen menu.alwaysSelectOpened=Immer geöffnete Datei/Klasse auswählen +#menu.dock_log=Dock log viewer menu.navigation=Navigation menu.text_search=Textsuche menu.class_search=Klassen-Suche @@ -143,6 +144,12 @@ comment_dialog.usage=Shift + Enter verwenden, um eine neue Zeile zu beginnen log_viewer.title=Log-Anzeige log_viewer.log_level=Log-Level: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=Über JADX diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 1f889cada..54089c50d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -9,6 +9,7 @@ menu.sync=Sync with editor menu.flatten=Show flatten packages menu.heapUsageBar=Show memory usage bar menu.alwaysSelectOpened=Always Select Opened File/Class +menu.dock_log=Dock log viewer menu.navigation=Navigation menu.text_search=Text search menu.class_search=Class search @@ -143,6 +144,12 @@ comment_dialog.usage=Use Shift + Enter for start a new line log_viewer.title=Log Viewer log_viewer.log_level=Log level: +log_viewer.mode=Mode: +log_viewer.modes=All|All scripts|Current script +log_viewer.hide=Hide +log_viewer.dock=Dock +log_viewer.undock=Undock +log_viewer.clear=Clear about_dialog.title=About JADX diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 6eb9ed0e6..a717a2d0d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -9,6 +9,7 @@ menu.sync=Sincronizar con el editor menu.flatten=Mostrar paquetes en vista plana #menu.heapUsageBar= #menu.alwaysSelectOpened=Always Select Opened File/Class +#menu.dock_log=Dock log viewer menu.navigation=Navegación menu.text_search=Buscar texto menu.class_search=Buscar clase @@ -143,6 +144,12 @@ usage_dialog.label=Usage for: log_viewer.title=Visor log log_viewer.log_level=Nivel log: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=Sobre JADX diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 8f2f7e13f..f161f311a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -9,6 +9,7 @@ menu.sync=에디터와 동기화 menu.flatten=플랫 패키지 표시 menu.heapUsageBar=메모리 사용량 표시 menu.alwaysSelectOpened=항상 열린 파일/클래스 선택 +#menu.dock_log=Dock log viewer menu.navigation=네비게이션 menu.text_search=텍스트 검색 menu.class_search=클래스 검색 @@ -143,6 +144,12 @@ comment_dialog.usage=Shift + Enter 를 입력해 새 라인에 입력 log_viewer.title=로그 뷰어 log_viewer.log_level=로그 레벨: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=JADX 정보 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 7c5e9a29f..cf6d7c206 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -9,6 +9,7 @@ menu.sync=Sincronizar com editor menu.flatten=Mostrar pacotes achatados menu.heapUsageBar=Mostrar uso de memória menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta +#menu.dock_log=Dock log viewer menu.navigation=Navegação menu.text_search=Buscar por texto menu.class_search=Buscar por classe @@ -143,6 +144,12 @@ comment_dialog.usage=Use Shift + Enter para pular uma linha log_viewer.title=Visualizador de log log_viewer.log_level=Nível do log: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=Sobre o JADX diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index aa3d6f6ec..e28517544 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -9,6 +9,7 @@ menu.sync=与编辑器同步 menu.flatten=展开显示代码包 menu.heapUsageBar=显示内存使用栏 menu.alwaysSelectOpened=始终选中打开的文件/类 +#menu.dock_log=Dock log viewer menu.navigation=导航 menu.text_search=文本搜索 menu.class_search=类名搜索 @@ -143,6 +144,12 @@ comment_dialog.usage=使用 Shift + Enter 换行 log_viewer.title=日志查看器 log_viewer.log_level=日志等级: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=关于 JADX diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 7e17835b5..94eb1f0c9 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -9,6 +9,7 @@ menu.sync=與編輯器同步 menu.flatten=展開顯示套件 menu.heapUsageBar=顯示記憶體使用率條 menu.alwaysSelectOpened=總是選擇已開啟的檔案/類別 +#menu.dock_log=Dock log viewer menu.navigation=瀏覽 menu.text_search=文字搜尋 menu.class_search=類別搜尋 @@ -143,6 +144,12 @@ comment_dialog.usage=按下 Shift + Enter 來換行 log_viewer.title=日誌檢視器 log_viewer.log_level=紀錄層級: +#log_viewer.mode=Mode: +#log_viewer.modes=All|All scripts|Current script +#log_viewer.hide=Hide +#log_viewer.dock=Dock +#log_viewer.undock=Undock +#log_viewer.clear=Clear about_dialog.title=關於 JADX diff --git a/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts index 6879dfeca..f61fafd80 100644 --- a/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts +++ b/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts @@ -101,7 +101,7 @@ fun generateFieldSnippet(fld: FieldNode): String { return """ ${generateClassSnippet(fld.parentClass)} ${fld.name} = ${fld.parentClass.name}.$rawFieldName.value; - """.trimIndent() + """.trimIndent() } fun isOverloaded(methodNode: MethodNode): Boolean { diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt index 7c8927abe..310645b24 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/JadxScriptTemplate.kt @@ -28,6 +28,8 @@ import kotlin.script.experimental.jvm.JvmDependency import kotlin.script.experimental.jvm.dependenciesFromCurrentContext import kotlin.script.experimental.jvm.jvm +const val JADX_SCRIPT_LOG_PREFIX = "JadxScript:" + @KotlinScript( fileExtension = "jadx.kts", compilationConfiguration = JadxScriptConfiguration::class @@ -36,7 +38,7 @@ abstract class JadxScriptTemplate( private val scriptData: JadxScriptData ) { val scriptName = scriptData.scriptName - val log = KotlinLogging.logger("JadxScript:$scriptName") + val log = KotlinLogging.logger("$JADX_SCRIPT_LOG_PREFIX$scriptName") fun getJadxInstance() = JadxScriptInstance(scriptData, log) diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt index 98aa8481a..d6ae9e46d 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/Runtime.kt @@ -27,7 +27,7 @@ class JadxScriptData( ) { val afterLoad: MutableList<() -> Unit> = ArrayList() - val scriptName get() = scriptFile.name.removeSuffix(".jadx.kts") + val scriptName = scriptFile.name.removeSuffix(".jadx.kts") } class JadxScriptInstance(