From bd75baef56493ab8f2ff2a3d0d2085291724a3ed Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:00:50 +0000 Subject: [PATCH] fix(gui): split loading and UI update for code area (#2682) --- .../src/main/java/jadx/core/utils/Utils.java | 21 ++++++--- .../jadx/gui/jobs/BackgroundExecutor.java | 6 ++- .../java/jadx/gui/jobs/IBackgroundTask.java | 7 +++ .../src/main/java/jadx/gui/jobs/LoadTask.java | 47 +++++++++++++++++++ .../main/java/jadx/gui/jobs/SilentTask.java | 32 +++++++++++++ .../jadx/gui/jobs/TaskWithExtraOnFinish.java | 11 +++-- .../gui/plugins/mappings/JInputMapping.java | 5 ++ .../gui/plugins/quark/QuarkReportNode.java | 5 ++ .../gui/plugins/script/ScriptCodeArea.java | 16 ++++--- .../jadx/gui/treemodel/ApkSignatureNode.java | 5 ++ .../main/java/jadx/gui/treemodel/JClass.java | 5 ++ .../java/jadx/gui/treemodel/JInputScript.java | 5 ++ .../jadx/gui/treemodel/JInputSmaliFile.java | 5 ++ .../main/java/jadx/gui/treemodel/JNode.java | 4 ++ .../java/jadx/gui/treemodel/JResource.java | 5 ++ .../src/main/java/jadx/gui/ui/MainWindow.java | 10 +--- .../gui/ui/codearea/AbstractCodeArea.java | 26 ++++++++-- .../gui/ui/codearea/BinaryContentPanel.java | 12 ++++- .../java/jadx/gui/ui/codearea/CodeArea.java | 26 ++++++++-- .../java/jadx/gui/ui/codearea/SmaliArea.java | 47 +++++++++++++------ .../jadx/gui/ui/startpage/StartPageNode.java | 5 ++ .../main/java/jadx/gui/ui/tab/TabbedPane.java | 13 ++--- .../java/jadx/gui/ui/tab/TabsController.java | 35 +++++--------- .../jadx/gui/ui/treenodes/SummaryNode.java | 5 ++ .../ui/treenodes/UndisplayedStringsNode.java | 7 ++- .../src/main/java/jadx/gui/utils/UiUtils.java | 6 +-- 26 files changed, 283 insertions(+), 88 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/LoadTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/SilentTask.java diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 0079bcaee..229b43053 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -514,9 +514,9 @@ public class Utils { } private static final class SimpleThreadFactory implements ThreadFactory { - private static final Logger LOG = LoggerFactory.getLogger(Utils.class); - private static final AtomicInteger POOL = new AtomicInteger(0); + private static final Thread.UncaughtExceptionHandler EXC_HANDLER = new SimpleUncaughtExceptionHandler(); + private final AtomicInteger number = new AtomicInteger(0); private final String name; @@ -529,15 +529,22 @@ public class Utils { Thread thread = new Thread(r, "jadx-" + name + '-' + POOL.incrementAndGet() + '-' + number.incrementAndGet()); - thread.setUncaughtExceptionHandler((t, e) -> { + thread.setUncaughtExceptionHandler(EXC_HANDLER); + return thread; + } + + private static class SimpleUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(SimpleUncaughtExceptionHandler.class); + + @Override + public void uncaughtException(Thread thread, Throwable e) { if (e instanceof OutOfMemoryError) { thread.interrupt(); - LOG.error("OutOfMemoryError in thread: {}, forcing interrupt", t.getName()); + LOG.error("OutOfMemoryError in thread: {}, forcing interrupt", thread.getName()); } else { - LOG.error("Uncaught thread exception, thread: {}", t.getName(), e); + LOG.error("Uncaught thread exception, thread: {}", thread.getName(), e); } - }); - return thread; + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index 36da45943..8a0ff7e52 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -182,7 +182,7 @@ public class BackgroundExecutor { jobsCount = taskExecutor.getTasksCount(); LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage()); - if (jobsCount != 1) { + if (jobsCount != 1 && !task.isSilent()) { progressPane.changeVisibility(this, true); } status = TaskStatus.STARTED; @@ -202,6 +202,10 @@ public class BackgroundExecutor { if (!taskExecutor.isRunning()) { return TaskStatus.COMPLETE; } + if (task.isSilent()) { + Thread.sleep(50); + continue; + } TaskStatus cancelStatus = cancelCheck.get(); if (cancelStatus != null) { performCancel(); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java index b1ede9d38..4b0de4912 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java @@ -55,4 +55,11 @@ public interface IBackgroundTask extends Cancelable { default @Nullable Consumer getProgressListener() { return null; } + + /** + * Silent task: don't show progress + */ + default boolean isSilent() { + return false; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/LoadTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/LoadTask.java new file mode 100644 index 000000000..b4e567485 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/LoadTask.java @@ -0,0 +1,47 @@ +package jadx.gui.jobs; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.tasks.TaskExecutor; +import jadx.gui.utils.NLS; + +/** + * Load task: prepare data in background task and use that data in UI task + */ +public class LoadTask extends CancelableBackgroundTask { + private final String title; + private final AtomicReference taskData; + private final Runnable bgTask; + private final Runnable uiTask; + + public LoadTask(Supplier loadBgTask, Consumer uiTask) { + this(NLS.str("progress.load"), loadBgTask, uiTask); + } + + public LoadTask(String title, Supplier loadBgTask, Consumer uiTask) { + this.title = title; + this.taskData = new AtomicReference<>(); + this.bgTask = () -> taskData.set(loadBgTask.get()); + this.uiTask = () -> uiTask.accept(taskData.get()); + } + + @Override + public String getTitle() { + return title; + } + + @Override + public ITaskExecutor scheduleTasks() { + TaskExecutor executor = new TaskExecutor(); + executor.addSequentialTask(bgTask); + return executor; + } + + @Override + public void onFinish(ITaskInfo taskInfo) { + uiTask.run(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/SilentTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/SilentTask.java new file mode 100644 index 000000000..14d15b3d1 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/SilentTask.java @@ -0,0 +1,32 @@ +package jadx.gui.jobs; + +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.tasks.TaskExecutor; + +/** + * Simple and short task, will not show progress + */ +public class SilentTask extends CancelableBackgroundTask { + private final Runnable task; + + public SilentTask(Runnable task) { + this.task = task; + } + + @Override + public boolean isSilent() { + return true; + } + + @Override + public String getTitle() { + return ""; + } + + @Override + public ITaskExecutor scheduleTasks() { + TaskExecutor executor = new TaskExecutor(); + executor.addSequentialTask(task); + return executor; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/TaskWithExtraOnFinish.java b/jadx-gui/src/main/java/jadx/gui/jobs/TaskWithExtraOnFinish.java index 4a0875233..e332b88dc 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/TaskWithExtraOnFinish.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/TaskWithExtraOnFinish.java @@ -1,5 +1,6 @@ package jadx.gui.jobs; +import java.util.Objects; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; @@ -13,15 +14,15 @@ public class TaskWithExtraOnFinish implements IBackgroundTask { private final IBackgroundTask task; private final Consumer extraOnFinish; - public TaskWithExtraOnFinish(IBackgroundTask task, Consumer extraOnFinish) { - this.task = task; - this.extraOnFinish = extraOnFinish; - } - public TaskWithExtraOnFinish(IBackgroundTask task, Runnable extraOnFinish) { this(task, s -> extraOnFinish.run()); } + public TaskWithExtraOnFinish(IBackgroundTask task, Consumer extraOnFinish) { + this.task = Objects.requireNonNull(task); + this.extraOnFinish = Objects.requireNonNull(extraOnFinish); + } + @Override public void onFinish(ITaskInfo taskInfo) { task.onFinish(taskInfo); diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/mappings/JInputMapping.java b/jadx-gui/src/main/java/jadx/gui/plugins/mappings/JInputMapping.java index ac0720e9a..d042f2dd9 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/mappings/JInputMapping.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/mappings/JInputMapping.java @@ -38,6 +38,11 @@ public class JInputMapping extends JEditableNode { this.name = mappingPath.getFileName().toString(); } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new CodeContentPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java index b086504a8..f6e708a19 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java @@ -53,6 +53,11 @@ public class QuarkReportNode extends JNode { return "Quark analysis report"; } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { try { diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java index d088577ae..e72db5947 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java @@ -4,6 +4,8 @@ import org.fife.ui.autocomplete.AutoCompletion; import org.jetbrains.annotations.NotNull; import jadx.api.ICodeInfo; +import jadx.gui.jobs.IBackgroundTask; +import jadx.gui.jobs.LoadTask; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JInputScript; import jadx.gui.ui.action.JadxAutoCompletion; @@ -48,12 +50,14 @@ public class ScriptCodeArea extends AbstractCodeArea { } @Override - public void load() { - if (getText().isEmpty()) { - setText(getCodeInfo().getCodeStr()); - setCaretPosition(0); - setLoaded(); - } + public IBackgroundTask getLoadTask() { + return new LoadTask<>( + () -> node.getCodeInfo().getCodeStr(), + code -> { + setText(code); + setCaretPosition(0); + setLoaded(); + }); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignatureNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignatureNode.java index 42fb4c76a..db3d21207 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignatureNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignatureNode.java @@ -83,6 +83,11 @@ public class ApkSignatureNode extends JNode { return "APK signature"; } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { ApkSignatureNode.tabbedPane = tabbedPane; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index f73a1fcdb..fb0597c6c 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -135,6 +135,11 @@ public class JClass extends JLoadableNode implements JRenameNode { return cls.getCodeInfo(); } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new ClassCodeContentPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java index de0e8f336..63f125c8a 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputScript.java @@ -36,6 +36,11 @@ public class JInputScript extends JEditableNode { this.name = scriptPath.getFileName().toString().replace(".jadx.kts", ""); } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new ScriptContentPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputSmaliFile.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputSmaliFile.java index 138b19611..c853ad87f 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JInputSmaliFile.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JInputSmaliFile.java @@ -35,6 +35,11 @@ public class JInputSmaliFile extends JEditableNode { return JInputFile.buildInputFilePopupMenu(mainWindow, filePath); } + @Override + public boolean hasContent() { + return true; + } + @Override public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { return new CodeContentPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index 8d12a0f9c..16e2eaa85 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -45,6 +45,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements ITreeNode, return null; } + public boolean hasContent() { + return false; + } + public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { return null; } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index d269b700b..86d55e8ff 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -152,6 +152,11 @@ public class JResource extends JLoadableNode { } } + @Override + public boolean hasContent() { + return resFile != null; + } + @Override public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { if (resFile == null) { 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 e9071c93d..b797508f7 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -120,15 +120,11 @@ import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.JadxSettingsWindow; import jadx.gui.tree.TreeExpansionService; import jadx.gui.treemodel.ApkSignatureNode; -import jadx.gui.treemodel.JInputFiles; -import jadx.gui.treemodel.JInputScripts; -import jadx.gui.treemodel.JInputs; import jadx.gui.treemodel.JLoadableNode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JRoot; -import jadx.gui.treemodel.JSources; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.codearea.AbstractCodeArea; @@ -920,11 +916,7 @@ public class MainWindow extends JFrame { } } else if (obj instanceof JNode) { JNode treeNode = (JNode) obj; - if (!(treeNode instanceof JPackage) - && !(treeNode instanceof JSources) - && !(treeNode instanceof JInputs) - && !(treeNode instanceof JInputFiles) - && !(treeNode instanceof JInputScripts)) { + if (treeNode.hasContent() || treeNode.getJParent() != null) { tabsController.codeJump(treeNode, true); return true; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 658743a80..19940185e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -13,6 +13,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.AbstractAction; import javax.swing.Action; @@ -45,6 +46,7 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.jobs.IBackgroundTask; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; @@ -83,7 +85,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { protected ContentPanel contentPanel; protected JNode node; - protected volatile boolean loaded = false; + private final AtomicBoolean loaded = new AtomicBoolean(false); public AbstractCodeArea(ContentPanel contentPanel, JNode node) { this.contentPanel = contentPanel; @@ -254,7 +256,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { private void addChangeUpdates(JEditableNode editableNode) { getDocument().addDocumentListener(new DocumentUpdateListener(ev -> { - if (loaded) { + if (loaded.get()) { editableNode.setChanged(true); } })); @@ -349,17 +351,33 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { public abstract ICodeInfo getCodeInfo(); + public void load() { + if (isLoaded()) { + return; + } + IBackgroundTask loadTask = getLoadTask(); + contentPanel.getMainWindow().getBackgroundExecutor().execute(loadTask); + } + /** * Implement in this method the code that loads and sets the content to be displayed * Call `setLoaded()` on load finish. */ - public abstract void load(); + public abstract IBackgroundTask getLoadTask(); public void setLoaded() { - this.loaded = true; + this.loaded.set(true); discardAllEdits(); // disable 'undo' action to empty state (before load) } + public void setUnLoaded() { + this.loaded.set(false); + } + + public boolean isLoaded() { + return loaded.get(); + } + /** * Implement in this method the code that reloads node from cache and sets the new content to be * displayed diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java index aed9d3ad9..cac7006e5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java @@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory; import jadx.api.ResourcesLoader; import jadx.api.resources.ResourceContentType; import jadx.gui.jobs.BackgroundExecutor; +import jadx.gui.jobs.IBackgroundTask; +import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.JNode; @@ -89,7 +91,7 @@ public class BinaryContentPanel extends AbstractCodeContentPanel { Component codePanel = getSelectedPanel(); if (codePanel instanceof CodeArea) { CodeArea codeArea = (CodeArea) codePanel; - bgExec.startLoading(codeArea::load); + codeArea.load(); } else { bgExec.startLoading(this::loadHexView); } @@ -106,11 +108,17 @@ public class BinaryContentPanel extends AbstractCodeContentPanel { @Override public void scrollToPos(int pos) { + UiUtils.uiThreadGuard(); BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor(); if (getNode().getContentType() == ResourceContentType.CONTENT_TEXT) { areaTabbedPane.setSelectedComponent(textCodePanel); AbstractCodeArea codeArea = textCodePanel.getCodeArea(); - bgExec.startLoading(codeArea::load, () -> codeArea.scrollToPos(pos)); + if (codeArea.isLoaded()) { + codeArea.scrollToPos(pos); + } else { + IBackgroundTask loadTask = codeArea.getLoadTask(); + bgExec.execute(new TaskWithExtraOnFinish(loadTask, () -> codeArea.scrollToPos(pos))); + } } else { areaTabbedPane.setSelectedComponent(hexPreviewPanel); bgExec.startLoading(this::loadHexView, () -> hexPreviewPanel.scrollToOffset(pos)); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index 13cf07208..b7636ffec 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -21,8 +21,12 @@ import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.metadata.ICodeAnnotation; import jadx.gui.JadxWrapper; +import jadx.gui.jobs.IBackgroundTask; +import jadx.gui.jobs.LoadTask; +import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JLoadableNode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; @@ -120,12 +124,24 @@ public final class CodeArea extends AbstractCodeArea { } @Override - public void load() { - if (getText().isEmpty()) { - setText(getCodeInfo().getCodeStr()); - setCaretPosition(0); - setLoaded(); + public IBackgroundTask getLoadTask() { + if (node instanceof JLoadableNode) { + IBackgroundTask loadTask = ((JLoadableNode) node).getLoadTask(); + if (loadTask != null) { + return new TaskWithExtraOnFinish(loadTask, () -> { + setText(getCodeInfo().getCodeStr()); + setCaretPosition(0); + setLoaded(); + }); + } } + return new LoadTask<>( + () -> getCodeInfo().getCodeStr(), + code -> { + setText(code); + setCaretPosition(0); + setLoaded(); + }); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index d86ddedc0..fb1226e60 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -37,6 +37,8 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.device.debugger.DbgUtils; +import jadx.gui.jobs.IBackgroundTask; +import jadx.gui.jobs.LoadTask; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -88,13 +90,15 @@ public final class SmaliArea extends AbstractCodeArea { } @Override - public void load() { - if (getText().isEmpty() || curVersion != shouldUseSmaliPrinterV2()) { - curVersion = shouldUseSmaliPrinterV2(); - model.load(); - setCaretPosition(0); - setLoaded(); - } + public IBackgroundTask getLoadTask() { + return new LoadTask<>( + () -> model.loadCode(), + code -> { + curVersion = shouldUseSmaliPrinterV2(); + model.loadUI(code); + setCaretPosition(0); + setLoaded(); + }); } @Override @@ -121,7 +125,10 @@ public final class SmaliArea extends AbstractCodeArea { if (model != null) { model.unload(); } - model = shouldUseSmaliPrinterV2() ? new DebugModel() : new NormalModel(this); + curVersion = shouldUseSmaliPrinterV2(); + model = curVersion ? new DebugModel() : new NormalModel(this); + setUnLoaded(); + load(); } public void scrollToDebugPos(int pos) { @@ -153,7 +160,9 @@ public final class SmaliArea extends AbstractCodeArea { } private abstract class SmaliModel { - abstract void load(); + abstract String loadCode(); + + abstract void loadUI(String code); abstract void unload(); @@ -173,20 +182,23 @@ public final class SmaliArea extends AbstractCodeArea { } private class NormalModel extends SmaliModel { - public NormalModel(SmaliArea smaliArea) { getContentPanel().getMainWindow().getEditorThemeManager().apply(smaliArea); setSyntaxEditingStyle(SYNTAX_STYLE_SMALI); } @Override - public void load() { - setText(getJClass().getSmali()); + public String loadCode() { + return getJClass().getSmali(); + } + + @Override + public void loadUI(String code) { + setText(code); } @Override public void unload() { - } } @@ -210,7 +222,12 @@ public final class SmaliArea extends AbstractCodeArea { } @Override - public void load() { + String loadCode() { + return DbgUtils.getSmaliCode(((JClass) node).getCls().getClassNode()); + } + + @Override + void loadUI(String code) { if (gutter == null) { gutter = RSyntaxUtilities.getGutter(SmaliArea.this); gutter.setBookmarkingEnabled(true); @@ -218,7 +235,7 @@ public final class SmaliArea extends AbstractCodeArea { Font baseFont = SmaliArea.super.getFont(); gutter.setLineNumberFont(baseFont.deriveFont(baseFont.getSize2D() - 1.0f)); } - setText(DbgUtils.getSmaliCode(((JClass) node).getCls().getClassNode())); + setText(code); loadV2Style(); loadBreakpoints(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPageNode.java b/jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPageNode.java index db0e59a42..05fc662c7 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPageNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPageNode.java @@ -12,6 +12,11 @@ import jadx.gui.utils.NLS; public class StartPageNode extends JNode { private static final long serialVersionUID = 8983134608645736174L; + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new StartPagePanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java index 96a967d8c..b8cd13249 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.jobs.SilentTask; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; @@ -221,7 +222,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener { private @Nullable ContentPanel showCode(JumpPosition jumpPos) { JNode jumpNode = jumpPos.getNode(); - ContentPanel contentPanel = getContentPanel(jumpNode); + ContentPanel contentPanel = getTabByNode(jumpNode); if (contentPanel == null) { return null; } @@ -314,11 +315,6 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener { return (TabComponent) component; } - private @Nullable ContentPanel getContentPanel(JNode node) { - controller.openTab(node); - return getTabByNode(node); - } - public void refresh(JNode node) { ContentPanel panel = getTabByNode(node); if (panel != null) { @@ -421,7 +417,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener { @Override public void onTabSelect(TabBlueprint blueprint) { - ContentPanel contentPanel = getContentPanel(blueprint.getNode()); + ContentPanel contentPanel = getTabByNode(blueprint.getNode()); if (contentPanel != null) { setSelectedComponent(contentPanel); } @@ -429,7 +425,8 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener { @Override public void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition position) { - showCode(position); + // queue task to wait completion of loading tasks + mainWindow.getBackgroundExecutor().execute(new SilentTask(() -> showCode(position))); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java index 5d756f752..adfa1e1ac 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java @@ -15,6 +15,7 @@ import jadx.api.JavaClass; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.SimpleTask; import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.treemodel.JClass; @@ -36,7 +37,9 @@ public class TabsController { public TabsController(MainWindow mainWindow) { this.mainWindow = mainWindow; - // addListener(new LogTabStates()); + if (UiUtils.JADX_GUI_DEBUG) { + addListener(new LogTabStates()); + } } public MainWindow getMainWindow() { @@ -59,11 +62,10 @@ public class TabsController { return openTab(node, false, false); } - public TabBlueprint openTab(JNode node, boolean hidden) { - return openTab(node, hidden, false); - } - public TabBlueprint openTab(JNode node, boolean hidden, boolean preview) { + if (!node.hasContent()) { + throw new JadxRuntimeException("Can't open tab for node without content"); + } TabBlueprint blueprint = getTabByNode(node); if (blueprint == null) { TabBlueprint newBlueprint = new TabBlueprint(node); @@ -77,10 +79,6 @@ public class TabsController { blueprint = newBlueprint; } setTabHiddenInternal(blueprint, hidden); - if (!blueprint.isCreated()) { - LOG.warn("No content panel for node: {}", node); - closeTabForce(blueprint); - } return blueprint; } @@ -89,10 +87,7 @@ public class TabsController { if (blueprint != null) { closeTab(blueprint.getNode()); } - - blueprint = openTab(node, false, true); - - return blueprint; + return openTab(node, false, true); } public void selectTab(JNode node) { @@ -125,6 +120,9 @@ public class TabsController { public void codeJump(JNode node, boolean fromTree) { JClass parentCls = node.getJParent(); if (parentCls != null) { + // handle jump to inner class, method or field: + // - load parent + // - search position and jump to it JavaClass cls = node.getJParent().getCls(); JavaClass origTopCls = cls.getOriginalTopParentClass(); JavaClass codeParent = cls.getTopParentClass(); @@ -134,19 +132,12 @@ public class TabsController { return; } } - - // Not an inline node, jump normally - if (node.getPos() > 0) { - codeJump(new JumpPosition(node), fromTree); - return; - } if (node.getRootClass() == null) { - // not a class, select tab without position scroll + // not a class, select tab (without position scroll) selectTab(node, fromTree); return; } - // node need loading - loadCodeWithUIAction(node.getRootClass(), () -> codeJump(new JumpPosition(node), fromTree)); + codeJump(new JumpPosition(node), fromTree); } private void loadCodeWithUIAction(JClass cls, Runnable action) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java index 3ce15f423..7b21b8f92 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java @@ -194,6 +194,11 @@ public class SummaryNode extends JNode { return String.format("%d (%.2f%%)", value, value * 100 / ((double) total)); } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new HtmlPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/UndisplayedStringsNode.java b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/UndisplayedStringsNode.java index 2b5edbb28..fbfa37bae 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/UndisplayedStringsNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/UndisplayedStringsNode.java @@ -1,6 +1,6 @@ package jadx.gui.ui.treenodes; -import javax.swing.*; +import javax.swing.Icon; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -19,6 +19,11 @@ public class UndisplayedStringsNode extends JNode { this.undisplayedStings = undisplayedStings; } + @Override + public boolean hasContent() { + return true; + } + @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new UndisplayedStringsPanel(tabbedPane, this); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index e03f0468c..e278fb959 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -430,17 +430,17 @@ public class UiUtils { * Uses single thread, so all tasks are ordered. */ public static void bgRun(Runnable runnable) { - BACKGROUND_THREAD.submit(runnable); + BACKGROUND_THREAD.execute(runnable); } public static void uiThreadGuard() { - if (!SwingUtilities.isEventDispatchThread()) { + if (JADX_GUI_DEBUG && !SwingUtilities.isEventDispatchThread()) { LOG.warn("Expect UI thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); } } public static void notUiThreadGuard() { - if (SwingUtilities.isEventDispatchThread()) { + if (JADX_GUI_DEBUG && SwingUtilities.isEventDispatchThread()) { LOG.warn("Expect background thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); } }