fix(gui): Quick Tabs Optimization (PR #2242)

* optimize tabs reorder

* restructure based on quick tabs architecture

* code formatting

* log all exceptions from background executor

* various improvements

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Mino
2024-08-30 20:33:05 +01:00
committed by GitHub
parent e63808bc4b
commit 9a39b70a46
28 changed files with 456 additions and 232 deletions
@@ -138,9 +138,17 @@ public class BackgroundExecutor {
task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
progressPane.setVisible(false);
task.onFinish(this);
try {
progressPane.setVisible(false);
task.onFinish(this);
} catch (Throwable e) {
LOG.error("Task onFinish failed", e);
status = TaskStatus.ERROR;
}
});
} catch (Throwable e) {
LOG.error("Task onDone failed", e);
status = TaskStatus.ERROR;
} finally {
taskComplete(id);
progressPane.changeVisibility(this, false);
@@ -23,7 +23,7 @@ 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.ui.tab.TabBlueprint;
import jadx.gui.utils.NLS;
public class LogPanel extends JPanel {
@@ -141,9 +141,9 @@ public class LogPanel extends JPanel {
}
private @Nullable String getCurrentScriptName() {
ContentPanel selectedCodePanel = mainWindow.getTabbedPane().getSelectedContentPanel();
if (selectedCodePanel != null) {
JNode node = selectedCodePanel.getNode();
TabBlueprint selectedTab = mainWindow.getTabsController().getSelectedTab();
if (selectedTab != null) {
JNode node = selectedTab.getNode();
if (node instanceof JInputScript) {
return node.getName();
}
@@ -179,7 +179,7 @@ public class GuiPluginContext implements JadxGuiContext {
return false;
}
commonContext.getMainWindow().getTabbedPane().codeJump(node);
commonContext.getMainWindow().getTabsController().codeJump(node);
return true;
}
@@ -114,7 +114,7 @@ public class QuarkManager {
root.replaceCustomNode(quarkNode);
root.update();
mainWindow.reloadTree();
mainWindow.getTabbedPane().showNode(quarkNode);
mainWindow.getTabsController().selectTab(quarkNode);
} catch (Exception e) {
UiUtils.errorMessage(mainWindow, "Failed to load Quark report.");
LOG.error("Failed to load Quark report.", e);
@@ -117,7 +117,7 @@ public class QuarkReportPanel extends ContentPanel {
Object node = getNodeUnderMouse(tree, event);
if (node instanceof MethodTreeNode) {
JMethod method = ((MethodTreeNode) node).getJMethod();
tabbedPane.codeJump(method);
tabbedPane.getTabsController().codeJump(method);
}
}
}
@@ -159,7 +159,7 @@ public class QuarkReportPanel extends ContentPanel {
@Override
public void loadSettings() {
Font settingsFont = getTabbedPane().getMainWindow().getSettings().getFont();
Font settingsFont = getMainWindow().getSettings().getFont();
this.font = settingsFont.deriveFont(settingsFont.getSize2D() + 1.f);
this.boldFont = font.deriveFont(Font.BOLD);
header.setFont(font);
@@ -279,7 +279,7 @@ public class QuarkReportPanel extends ContentPanel {
String[] parts = removeQuotes(descr).split(" ", 3);
String cls = Utils.cleanObjectName(parts[0].replace('$', '.'));
String mth = parts[1] + parts[2].replace(" ", "");
MainWindow mainWindow = getTabbedPane().getMainWindow();
MainWindow mainWindow = getMainWindow();
JadxWrapper wrapper = mainWindow.getWrapper();
JavaClass javaClass = wrapper.searchJavaClassByRawName(cls);
if (javaClass == null) {
@@ -25,8 +25,8 @@ public class ScriptCodeArea extends AbstractCodeArea {
setCodeFoldingEnabled(true);
setCloseCurlyBraces(true);
shortcutsController = contentPanel.getTabbedPane().getMainWindow().getShortcutsController();
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
shortcutsController = contentPanel.getMainWindow().getShortcutsController();
JadxSettings settings = contentPanel.getMainWindow().getSettings();
autoCompletion = addAutoComplete(settings);
}
@@ -232,7 +232,7 @@ public class ScriptContentPanel extends AbstractCodeContentPanel {
}
private void showScriptLog() {
getTabbedPane().getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName()));
getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName()));
}
@Override
@@ -40,8 +40,7 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return null;
}
@Nullable
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) {
return null;
}
@@ -139,6 +139,8 @@ 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.tab.EditorSyncManager;
import jadx.gui.ui.tab.NavigationController;
import jadx.gui.ui.tab.QuickTabsTree;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.TabsController;
@@ -195,6 +197,10 @@ public class MainWindow extends JFrame {
private final transient CacheManager cacheManager;
private final transient BackgroundExecutor backgroundExecutor;
private final TabsController tabsController;
private final NavigationController navController;
private final EditorSyncManager editorSyncManager;
private transient @NotNull JadxProject project;
private transient JadxGuiAction newProjectAction;
@@ -209,7 +215,6 @@ public class MainWindow extends JFrame {
private JTree tree;
private DefaultTreeModel treeModel;
private JRoot treeRoot;
private TabsController tabsController;
private TabbedPane tabbedPane;
private HeapUsageBar heapUsageBar;
private transient boolean treeReloading;
@@ -238,7 +243,7 @@ public class MainWindow extends JFrame {
private boolean loaded;
private boolean settingsOpen = false;
private ShortcutsController shortcutsController;
private final ShortcutsController shortcutsController;
private JadxMenuBar menuBar;
private JMenu pluginsMenu;
@@ -253,16 +258,19 @@ public class MainWindow extends JFrame {
this.renameMappings = new RenameMappingsGui(this);
this.cacheManager = new CacheManager(settings);
this.shortcutsController = new ShortcutsController(settings);
this.tabsController = new TabsController(this);
this.navController = new NavigationController(this);
JadxEventQueue.register();
resetCache();
FontUtils.registerBundledFonts();
setEditorTheme(settings.getEditorThemePath());
initUI();
this.editorSyncManager = new EditorSyncManager(this, tabbedPane);
this.backgroundExecutor = new BackgroundExecutor(settings, progressPane);
initMenuAndToolbar();
UiUtils.setWindowIcons(this);
shortcutsController.registerMouseEventListener(this);
this.shortcutsController.registerMouseEventListener(this);
loadSettings();
update();
@@ -288,7 +296,7 @@ public class MainWindow extends JFrame {
private void processCommandLineArgs() {
if (settings.getFiles().isEmpty()) {
tabbedPane.showNode(new StartPageNode());
tabsController.selectTab(new StartPageNode());
} else {
open(FileUtils.fileNamesToPaths(settings.getFiles()), this::handleSelectClassOption);
}
@@ -306,7 +314,7 @@ public class MainWindow extends JFrame {
NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE);
return;
}
tabbedPane.codeJump(cacheObject.getNodeCache().makeFrom(javaNode));
tabsController.codeJump(cacheObject.getNodeCache().makeFrom(javaNode));
}
}
@@ -517,7 +525,7 @@ public class MainWindow extends JFrame {
private void loadFiles(Runnable onFinish) {
if (project.getFilePaths().isEmpty()) {
tabbedPane.showNode(new StartPageNode());
tabsController.selectTab(new StartPageNode());
return;
}
AtomicReference<Exception> wrapperException = new AtomicReference<>();
@@ -562,6 +570,8 @@ public class MainWindow extends JFrame {
private void closeAll() {
notifyLoadListeners(false);
cancelBackgroundJobs();
navController.reset();
tabbedPane.reset();
clearTree();
resetCache();
LogCollector.getInstance().reset();
@@ -783,7 +793,6 @@ public class MainWindow extends JFrame {
}
private void clearTree() {
tabbedPane.reset();
treeRoot = null;
treeModel.setRoot(null);
treeModel.reload();
@@ -864,15 +873,17 @@ public class MainWindow extends JFrame {
JResource res = (JResource) obj;
ResourceFile resFile = res.getResFile();
if (resFile != null && JResource.isSupportedForView(resFile.getType())) {
return tabbedPane.showNode(res);
tabsController.selectTab(res);
return true;
}
} else if (obj instanceof JNode) {
JNode node = (JNode) obj;
if (node.getRootClass() != null) {
tabbedPane.codeJump(node);
tabsController.codeJump(node);
return true;
}
return tabbedPane.showNode(node);
tabsController.selectTab(node);
return true;
}
} catch (Exception e) {
LOG.error("Content loading error", e);
@@ -901,12 +912,8 @@ public class MainWindow extends JFrame {
return null;
}
public void syncWithEditor() {
ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel();
if (selectedContentPanel == null) {
return;
}
JNode node = selectedContentPanel.getNode();
// TODO: extract tree component into new class
public void selectNodeInTree(JNode node) {
if (node.getParent() == null && treeRoot != null) {
// node not register in tree
node = treeRoot.searchNode(node);
@@ -962,7 +969,7 @@ public class MainWindow extends JFrame {
if (mainActivityClass == null) {
throw new JadxRuntimeException("Failed to find main activity class: " + results.getMainActivity());
}
tabbedPane.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass));
tabsController.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass));
} catch (Exception e) {
LOG.error("Main activity not found", e);
JOptionPane.showMessageDialog(MainWindow.this,
@@ -992,7 +999,7 @@ public class MainWindow extends JFrame {
if (applicationClass == null) {
throw new JadxRuntimeException("Failed to find application class: " + results.getApplication());
}
tabbedPane.codeJump(getCacheObject().getNodeCache().makeFrom(applicationClass));
tabsController.codeJump(getCacheObject().getNodeCache().makeFrom(applicationClass));
} catch (Exception e) {
LOG.error("Application not found", e);
JOptionPane.showMessageDialog(MainWindow.this,
@@ -1042,7 +1049,7 @@ public class MainWindow extends JFrame {
alwaysSelectOpened.addActionListener(event -> {
settings.setAlwaysSelectOpened(!settings.isAlwaysSelectOpened());
if (settings.isAlwaysSelectOpened()) {
this.syncWithEditor();
this.editorSyncManager.sync();
}
});
@@ -1061,7 +1068,7 @@ public class MainWindow extends JFrame {
setQuickTabsVisibility(true);
}
JadxGuiAction syncAction = new JadxGuiAction(ActionModel.SYNC, this::syncWithEditor);
JadxGuiAction syncAction = new JadxGuiAction(ActionModel.SYNC, this.editorSyncManager::sync);
JadxGuiAction textSearchAction = new JadxGuiAction(ActionModel.TEXT_SEARCH, this::textSearch);
JadxGuiAction clsSearchAction = new JadxGuiAction(ActionModel.CLASS_SEARCH,
() -> SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS));
@@ -1085,10 +1092,10 @@ public class MainWindow extends JFrame {
JadxGuiAction showLogAction = new JadxGuiAction(ActionModel.SHOW_LOG,
() -> showLogViewer(LogOptions.current()));
JadxGuiAction aboutAction = new JadxGuiAction(ActionModel.ABOUT, () -> new AboutDialog().setVisible(true));
JadxGuiAction backAction = new JadxGuiAction(ActionModel.BACK, tabbedPane::navBack);
JadxGuiAction backVariantAction = new JadxGuiAction(ActionModel.BACK_V, tabbedPane::navBack);
JadxGuiAction forwardAction = new JadxGuiAction(ActionModel.FORWARD, tabbedPane::navForward);
JadxGuiAction forwardVariantAction = new JadxGuiAction(ActionModel.FORWARD_V, tabbedPane::navForward);
JadxGuiAction backAction = new JadxGuiAction(ActionModel.BACK, navController::navBack);
JadxGuiAction backVariantAction = new JadxGuiAction(ActionModel.BACK_V, navController::navBack);
JadxGuiAction forwardAction = new JadxGuiAction(ActionModel.FORWARD, navController::navForward);
JadxGuiAction forwardVariantAction = new JadxGuiAction(ActionModel.FORWARD_V, navController::navForward);
JadxGuiAction quarkAction = new JadxGuiAction(ActionModel.QUARK,
() -> new QuarkDialog(MainWindow.this).setVisible(true));
JadxGuiAction openDeviceAction = new JadxGuiAction(ActionModel.OPEN_DEVICE,
@@ -1345,7 +1352,6 @@ public class MainWindow extends JFrame {
leftPane.add(bottomPane, BorderLayout.PAGE_END);
treeSplitPane.setLeftComponent(leftPane);
tabsController = new TabsController(this);
tabbedPane = new TabbedPane(this, tabsController);
tabbedPane.setMinimumSize(new Dimension(150, 150));
new TabDndController(tabbedPane, settings);
@@ -1581,6 +1587,10 @@ public class MainWindow extends JFrame {
return tabsController;
}
public NavigationController getNavController() {
return navController;
}
public JadxSettings getSettings() {
return settings;
}
@@ -90,7 +90,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
applyEditableProperties(node);
loadSettings();
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
JadxSettings settings = contentPanel.getMainWindow().getSettings();
setLineWrap(settings.isCodeAreaLineWrap());
ZoomActions.register(this, settings, this::loadSettings);
@@ -147,7 +147,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
private void appendWrapLineMenu(JPopupMenu popupMenu) {
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
JadxSettings settings = contentPanel.getMainWindow().getSettings();
popupMenu.addSeparator();
JCheckBoxMenuItem wrapItem = new JCheckBoxMenuItem(NLS.str("popup.line_wrap"), getLineWrap());
wrapItem.setAction(new AbstractAction(NLS.str("popup.line_wrap")) {
@@ -378,7 +378,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
public void loadSettings() {
loadCommonSettings(contentPanel.getTabbedPane().getMainWindow(), this);
loadCommonSettings(contentPanel.getMainWindow(), this);
}
public void scrollToPos(int pos) {
@@ -87,7 +87,7 @@ public final class CodeArea extends AbstractCodeArea {
int offs = viewToModel2D(point);
JNode node = getJNodeAtOffset(adjustOffsetForWordToken(offs));
if (node != null) {
contentPanel.getTabbedPane().codeJump(node);
contentPanel.getTabsController().codeJump(node);
}
}
@@ -332,7 +332,7 @@ public final class CodeArea extends AbstractCodeArea {
}
public MainWindow getMainWindow() {
return contentPanel.getTabbedPane().getMainWindow();
return contentPanel.getMainWindow();
}
public JadxWrapper getJadxWrapper() {
@@ -82,7 +82,7 @@ public class CodePanel extends JPanel {
AbstractAction globalSearchAction = new AbstractAction(NLS.str("popup.search_global", "")) {
@Override
public void actionPerformed(ActionEvent e) {
MainWindow mainWindow = codeArea.getContentPanel().getTabbedPane().getMainWindow();
MainWindow mainWindow = codeArea.getContentPanel().getMainWindow();
SearchDialog.searchText(mainWindow, codeArea.getSelectedText());
}
};
@@ -12,6 +12,6 @@ public final class GoToDeclarationAction extends JNodeAction {
@Override
public void runAction(JNode node) {
getCodeArea().getContentPanel().getTabbedPane().codeJump(node);
getCodeArea().getContentPanel().getTabsController().codeJump(node);
}
}
@@ -85,10 +85,10 @@ public class HexArea extends AbstractCodeArea {
}
private void applyTheme() {
Font font = getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont();
Font font = getContentPanel().getMainWindow().getSettings().getSmaliFont();
setFont(font);
Theme theme = contentPanel.getTabbedPane().getMainWindow().getEditorTheme();
Theme theme = contentPanel.getMainWindow().getEditorTheme();
if (hexPreviewPanel != null) {
hexPreviewPanel.applyTheme(theme, font);
}
@@ -74,7 +74,7 @@ public final class SmaliArea extends AbstractCodeArea {
@Override
public void actionPerformed(ActionEvent e) {
JadxSettings settings = getContentPanel().getTabbedPane().getMainWindow().getSettings();
JadxSettings settings = getContentPanel().getMainWindow().getSettings();
settings.setSmaliAreaShowBytecode(!settings.getSmaliAreaShowBytecode());
contentPanel.getTabbedPane().getTabs().forEach(v -> {
if (v instanceof ClassCodeContentPanel) {
@@ -127,7 +127,7 @@ public final class SmaliArea extends AbstractCodeArea {
}
public void scrollToDebugPos(int pos) {
getContentPanel().getTabbedPane().getMainWindow()
getContentPanel().getMainWindow()
.getSettings().setSmaliAreaShowBytecode(true); // don't sync when it's set programmatically.
cbUseSmaliV2.setState(shouldUseSmaliPrinterV2());
if (!(model instanceof DebugModel)) {
@@ -151,7 +151,7 @@ public final class SmaliArea extends AbstractCodeArea {
}
private boolean shouldUseSmaliPrinterV2() {
return getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliAreaShowBytecode();
return getContentPanel().getMainWindow().getSettings().getSmaliAreaShowBytecode();
}
private abstract class SmaliModel {
@@ -177,7 +177,7 @@ public final class SmaliArea extends AbstractCodeArea {
private class NormalModel extends SmaliModel {
public NormalModel() {
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
Theme theme = getContentPanel().getMainWindow().getEditorTheme();
setSyntaxScheme(theme.scheme);
setSyntaxEditingStyle(SYNTAX_STYLE_SMALI);
}
@@ -323,16 +323,16 @@ public final class SmaliArea extends AbstractCodeArea {
public SmaliV2Style(SmaliArea smaliArea) {
super(true);
curTheme = smaliArea.getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
curTheme = smaliArea.getContentPanel().getMainWindow().getEditorTheme();
updateTheme();
}
public Font getFont() {
return getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont();
return getContentPanel().getMainWindow().getSettings().getSmaliFont();
}
public boolean refreshTheme() {
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
Theme theme = getContentPanel().getMainWindow().getEditorTheme();
boolean refresh = theme != curTheme;
if (refresh) {
curTheme = theme;
@@ -52,7 +52,7 @@ import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.TabsController;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
@@ -67,7 +67,7 @@ public abstract class CommonSearchDialog extends JFrame {
private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class);
private static final long serialVersionUID = 8939332306115370276L;
protected final transient TabbedPane tabbedPane;
protected final transient TabsController tabsController;
protected final transient CacheObject cache;
protected final transient MainWindow mainWindow;
protected final transient Font codeFont;
@@ -84,7 +84,7 @@ public abstract class CommonSearchDialog extends JFrame {
public CommonSearchDialog(MainWindow mainWindow, String title) {
this.mainWindow = mainWindow;
this.tabbedPane = mainWindow.getTabbedPane();
this.tabsController = mainWindow.getTabsController();
this.cache = mainWindow.getCacheObject();
this.codeFont = mainWindow.getSettings().getFont();
this.windowTitle = title;
@@ -145,9 +145,9 @@ public abstract class CommonSearchDialog extends JFrame {
protected void openItem(JNode node) {
if (node instanceof JResSearchNode) {
JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos());
tabbedPane.codeJump(jmpPos);
tabsController.codeJump(jmpPos);
} else {
tabbedPane.codeJump(node);
tabsController.codeJump(node);
}
if (!mainWindow.getSettings().getKeepCommonDialogOpen()) {
dispose();
@@ -7,7 +7,9 @@ import org.jetbrains.annotations.Nullable;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.TabsController;
public abstract class ContentPanel extends JPanel {
@@ -27,6 +29,14 @@ public abstract class ContentPanel extends JPanel {
return tabbedPane;
}
public TabsController getTabsController() {
return tabbedPane.getTabsController();
}
public MainWindow getMainWindow() {
return tabbedPane.getMainWindow();
}
public JNode getNode() {
return node;
}
@@ -34,7 +34,7 @@ public final class HtmlPanel extends ContentPanel {
@Override
public void loadSettings() {
JadxSettings settings = getTabbedPane().getMainWindow().getSettings();
JadxSettings settings = getMainWindow().getSettings();
textArea.setFont(settings.getFont());
}
@@ -445,7 +445,7 @@ public class JDebuggerPanel extends JPanel {
}
public void scrollToSmaliLine(JClass cls, int pos, boolean debugMode) {
SwingUtilities.invokeLater(() -> getMainWindow().getTabbedPane().smaliJump(cls, pos, debugMode));
SwingUtilities.invokeLater(() -> getMainWindow().getTabsController().smaliJump(cls, pos, debugMode));
}
public void resetAllDebuggingInfo() {
@@ -0,0 +1,33 @@
package jadx.gui.ui.tab;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ContentPanel;
public class EditorSyncManager implements ITabStatesListener {
private final MainWindow mainWindow;
private final TabbedPane tabbedPane;
public EditorSyncManager(MainWindow mainWindow, TabbedPane tabbedPane) {
this.mainWindow = mainWindow;
this.tabbedPane = tabbedPane;
mainWindow.getTabsController().addListener(this);
}
public void sync() {
ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel();
if (selectedContentPanel != null) {
mainWindow.selectNodeInTree(selectedContentPanel.getNode());
}
}
@Override
public void onTabSelect(TabBlueprint blueprint) {
if (mainWindow.getSettings().isAlwaysSelectOpened()) {
// verify that tab opened for this blueprint (some nodes don't open tab with content)
ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel();
if (selectedContentPanel != null && selectedContentPanel.getNode().equals(blueprint.getNode())) {
sync();
}
}
}
}
@@ -3,27 +3,47 @@ package jadx.gui.ui.tab;
import java.util.List;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.utils.JumpPosition;
public interface ITabStatesListener {
void onTabOpen(TabBlueprint blueprint);
void onTabSelect(TabBlueprint blueprint);
default void onTabOpen(TabBlueprint blueprint) {
}
void onTabClose(TabBlueprint blueprint);
default void onTabSelect(TabBlueprint blueprint) {
}
void onTabPositionFirst(TabBlueprint blueprint);
default void onTabCodeJump(TabBlueprint blueprint, JumpPosition position) {
}
void onTabPinChange(TabBlueprint blueprint);
default void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) {
}
void onTabBookmarkChange(TabBlueprint blueprint);
default void onTabClose(TabBlueprint blueprint) {
}
void onTabVisibilityChange(TabBlueprint blueprint);
default void onTabPositionFirst(TabBlueprint blueprint) {
}
void onTabRestore(TabBlueprint blueprint, EditorViewState viewState);
default void onTabPinChange(TabBlueprint blueprint) {
}
void onTabsRestoreDone();
default void onTabBookmarkChange(TabBlueprint blueprint) {
}
void onTabsReorder(List<TabBlueprint> blueprints);
default void onTabVisibilityChange(TabBlueprint blueprint) {
}
default void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) {
}
default void onTabsRestoreDone() {
}
default void onTabsReorder(List<TabBlueprint> blueprints) {
}
default void onTabSave(TabBlueprint blueprint, EditorViewState viewState) {
}
void onTabSave(TabBlueprint blueprint, EditorViewState viewState);
}
@@ -0,0 +1,81 @@
package jadx.gui.ui.tab;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.utils.JumpPosition;
/**
* Utility class to log events from TabsController by implementing ITabStatesListener.
*/
public class LogTabStates implements ITabStatesListener {
private static final Logger LOG = LoggerFactory.getLogger(LogTabStates.class);
@Override
public void onTabBookmarkChange(TabBlueprint blueprint) {
LOG.debug("onTabBookmarkChange: blueprint={}", blueprint);
}
@Override
public void onTabClose(TabBlueprint blueprint) {
LOG.debug("onTabClose: blueprint={}", blueprint);
}
@Override
public void onTabCodeJump(TabBlueprint blueprint, JumpPosition position) {
LOG.debug("onTabCodeJump: blueprint={}, position={}", blueprint, position);
}
@Override
public void onTabOpen(TabBlueprint blueprint) {
LOG.debug("onTabOpen: blueprint={}", blueprint);
}
@Override
public void onTabPinChange(TabBlueprint blueprint) {
LOG.debug("onTabPinChange: blueprint={}", blueprint);
}
@Override
public void onTabPositionFirst(TabBlueprint blueprint) {
LOG.debug("onTabPositionFirst: blueprint={}", blueprint);
}
@Override
public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) {
LOG.debug("onTabRestore: blueprint={}, viewState={}", blueprint, viewState);
}
@Override
public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) {
LOG.debug("onTabSave: blueprint={}, viewState={}", blueprint, viewState);
}
@Override
public void onTabSelect(TabBlueprint blueprint) {
LOG.debug("onTabSelect: blueprint={}", blueprint);
}
@Override
public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) {
LOG.debug("onTabSmaliJump: blueprint={}, pos={}, debugMode={}", blueprint, pos, debugMode);
}
@Override
public void onTabsReorder(List<TabBlueprint> blueprints) {
LOG.debug("onTabsReorder: blueprints={}", blueprints);
}
@Override
public void onTabsRestoreDone() {
LOG.debug("onTabsRestoreDone");
}
@Override
public void onTabVisibilityChange(TabBlueprint blueprint) {
LOG.debug("onTabVisibilityChange: blueprint={}", blueprint);
}
}
@@ -0,0 +1,73 @@
package jadx.gui.ui.tab;
import org.jetbrains.annotations.Nullable;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition;
/**
* TODO: Save jumps history into project file to restore after reload or reopen
*/
public class NavigationController implements ITabStatesListener {
private final transient MainWindow mainWindow;
private final transient JumpManager jumps = new JumpManager();
public NavigationController(MainWindow mainWindow) {
this.mainWindow = mainWindow;
mainWindow.getTabsController().addListener(this);
}
public void navBack() {
if (jumps.size() > 1) {
jumps.updateCurPosition(mainWindow.getTabbedPane().getCurrentPosition());
}
jump(jumps.getPrev());
}
public void navForward() {
if (jumps.size() > 1) {
jumps.updateCurPosition(mainWindow.getTabbedPane().getCurrentPosition());
}
jump(jumps.getNext());
}
private void jump(@Nullable JumpPosition pos) {
if (pos != null) {
mainWindow.getTabsController().codeJump(pos);
}
}
@Override
public void onTabCodeJump(TabBlueprint blueprint, JumpPosition position) {
if (position.equals(jumps.getCurrent())) {
// ignore self-initiated jumps
return;
}
saveCurrentPosition();
jumps.addPosition(position);
}
@Override
public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) {
saveCurrentPosition();
// TODO: save smali jump
}
private void saveCurrentPosition() {
JumpPosition curPos = mainWindow.getTabbedPane().getCurrentPosition();
if (curPos != null) {
jumps.addPosition(curPos);
}
}
public void reset() {
jumps.reset();
}
public void dispose() {
reset();
mainWindow.getTabsController().removeListener(this);
}
}
@@ -6,7 +6,6 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
@@ -20,7 +19,6 @@ import javax.swing.tree.TreeNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.utils.UiUtils;
public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSelectionListener {
@@ -180,11 +178,6 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele
}
}
@Override
public void onTabSelect(TabBlueprint blueprint) {
}
@Override
public void onTabClose(TabBlueprint blueprint) {
removeJNode(openParentNode, blueprint.getNode());
@@ -192,11 +185,6 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele
removeJNode(bookmarkParentNode, blueprint.getNode());
}
@Override
public void onTabPositionFirst(TabBlueprint blueprint) {
}
@Override
public void onTabPinChange(TabBlueprint blueprint) {
JNode node = blueprint.getNode();
@@ -227,26 +215,6 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele
}
}
@Override
public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) {
}
@Override
public void onTabsRestoreDone() {
}
@Override
public void onTabsReorder(List<TabBlueprint> blueprints) {
}
@Override
public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) {
}
private class Root extends DefaultMutableTreeNode {
}
@@ -1,5 +1,7 @@
package jadx.gui.ui.tab;
import java.util.Objects;
import jadx.gui.treemodel.JNode;
public class TabBlueprint {
@@ -9,7 +11,7 @@ public class TabBlueprint {
private boolean hidden;
public TabBlueprint(JNode node) {
this.node = node;
this.node = Objects.requireNonNull(node);
}
public JNode getNode() {
@@ -47,4 +49,29 @@ public class TabBlueprint {
public void setHidden(boolean hidden) {
this.hidden = hidden;
}
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TabBlueprint)) {
return false;
}
return node.equals(((TabBlueprint) o).node);
}
@Override
public int hashCode() {
return node.hashCode();
}
@Override
public String toString() {
return "TabBlueprint{node=" + node
+ ", bookmarked=" + bookmarked
+ ", pinned=" + pinned
+ ", hidden=" + hidden
+ '}';
}
}
@@ -8,10 +8,10 @@ import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
@@ -20,10 +20,6 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.treemodel.JClass;
import jadx.gui.treemodel.JNode;
@@ -38,9 +34,7 @@ import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.ui.tab.dnd.TabDndController;
import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class TabbedPane extends JTabbedPane implements ITabStatesListener {
@@ -52,8 +46,6 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
private final transient TabsController controller;
private final transient Map<JNode, ContentPanel> tabsMap = new HashMap<>();
private final transient JumpManager jumps = new JumpManager();
private transient ContentPanel curTab;
private transient ContentPanel lastTab;
@@ -212,63 +204,8 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
return mainWindow;
}
/**
* Jump to node definition
*/
public void codeJump(JNode node) {
JClass parentCls = node.getJParent();
if (parentCls != null) {
JavaClass cls = node.getJParent().getCls();
JavaClass origTopCls = cls.getOriginalTopParentClass();
JavaClass codeParent = cls.getTopParentClass();
if (!Objects.equals(codeParent, origTopCls)) {
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
jumpCls::loadNode, // load code in background
status -> {
// search original node in jump class
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode();
if (declNode.equals(node.getJavaNode().getCodeNodeRef())) {
codeJump(new JumpPosition(jumpCls, pos));
return true;
}
}
return null;
});
});
return;
}
}
// Not an inline node, jump normally
if (node.getPos() != 0 || node.getRootClass() == null) {
codeJump(new JumpPosition(node));
return;
}
// node need loading
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
() -> node.getRootClass().getCodeInfo(), // run heavy loading in background
status -> codeJump(new JumpPosition(node)));
}
/**
* Prefer {@link TabbedPane#codeJump(JNode)} method
*/
public void codeJump(JumpPosition pos) {
saveJump(pos);
showCode(pos);
}
private void saveJump(JumpPosition pos) {
JumpPosition curPos = getCurrentPosition();
if (curPos != null) {
jumps.addPosition(curPos);
jumps.addPosition(pos);
}
public TabsController getTabsController() {
return controller;
}
private @Nullable ContentPanel showCode(JumpPosition jumpPos) {
@@ -280,15 +217,6 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
return contentPanel;
}
public boolean showNode(JNode node) {
final ContentPanel contentPanel = getContentPanel(node);
if (contentPanel == null) {
return false;
}
selectTab(contentPanel);
return true;
}
private void scrollToPos(ContentPanel contentPanel, int pos) {
if (pos == 0) {
LOG.warn("Ignore zero jump!", new JadxRuntimeException());
@@ -305,7 +233,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
controller.selectTab(contentPanel.getNode());
}
public void smaliJump(JClass cls, int pos, boolean debugMode) {
private void smaliJump(JClass cls, int pos, boolean debugMode) {
ContentPanel panel = getTabByNode(cls);
if (panel == null) {
panel = showCode(new JumpPosition(cls, 1));
@@ -325,8 +253,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
smaliArea.requestFocus();
}
@Nullable
public JumpPosition getCurrentPosition() {
public @Nullable JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedContentPanel();
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
return ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea().getCurrentPosition();
@@ -334,26 +261,6 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
return null;
}
public void navBack() {
if (jumps.size() > 1) {
jumps.updateCurPosition(getCurrentPosition());
}
JumpPosition pos = jumps.getPrev();
if (pos != null) {
showCode(pos);
}
}
public void navForward() {
if (jumps.size() > 1) {
jumps.updateCurPosition(getCurrentPosition());
}
JumpPosition pos = jumps.getNext();
if (pos != null) {
showCode(pos);
}
}
private void addContentPanel(ContentPanel contentPanel) {
tabsMap.put(contentPanel.getNode(), contentPanel);
int tabCount = getTabCount();
@@ -382,7 +289,15 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
}
public @Nullable TabComponent getTabComponentByNode(JNode node) {
Component component = getTabComponentAt(indexOfComponent(getTabByNode(node)));
ContentPanel contentPanel = getTabByNode(node);
if (contentPanel == null) {
return null;
}
int index = indexOfComponent(contentPanel);
if (index == -1) {
return null;
}
Component component = getTabComponentAt(index);
if (!(component instanceof TabComponent)) {
return null;
}
@@ -460,7 +375,6 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
public void reset() {
closeAllTabs();
tabsMap.clear();
jumps.reset();
curTab = null;
lastTab = null;
FocusManager.reset();
@@ -485,16 +399,30 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
return;
}
ContentPanel newPanel = blueprint.getNode().getContentPanel(this);
FocusManager.listen(newPanel);
addContentPanel(newPanel);
if (newPanel != null) {
FocusManager.listen(newPanel);
addContentPanel(newPanel);
}
}
@Override
public void onTabSelect(TabBlueprint blueprint) {
ContentPanel contentPanel = getContentPanel(blueprint.getNode());
setSelectedComponent(contentPanel);
if (mainWindow.getSettings().isAlwaysSelectOpened()) {
mainWindow.syncWithEditor();
if (contentPanel != null) {
setSelectedComponent(contentPanel);
}
}
@Override
public void onTabCodeJump(TabBlueprint blueprint, JumpPosition position) {
showCode(position);
}
@Override
public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) {
JNode node = blueprint.getNode();
if (node instanceof JClass) {
smaliJump((JClass) node, pos, debugMode);
}
}
@@ -564,25 +492,19 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
}
}
@Override
public void onTabsRestoreDone() {
}
@Override
public void onTabsReorder(List<TabBlueprint> blueprints) {
List<TabBlueprint> newBlueprints = new ArrayList<>();
List<TabBlueprint> newBlueprints = new ArrayList<>(blueprints.size());
for (ContentPanel contentPanel : getTabs()) {
Optional<TabBlueprint> blueprintFindResult = blueprints.stream()
.filter(b -> b.getNode() == contentPanel.getNode())
.findFirst();
if (blueprintFindResult.isPresent()) {
TabBlueprint blueprint = blueprintFindResult.get();
blueprints.remove(blueprint);
TabBlueprint blueprint = controller.getTabByNode(contentPanel.getNode());
if (blueprint != null) {
newBlueprints.add(blueprint);
}
}
// Add back hidden tabs
newBlueprints.addAll(blueprints);
Set<TabBlueprint> set = new LinkedHashSet<>(blueprints);
newBlueprints.forEach(set::remove);
newBlueprints.addAll(set);
blueprints.clear();
blueprints.addAll(newBlueprints);
@@ -4,22 +4,34 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JavaClass;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
public class TabsController {
private final transient MainWindow mainWindow;
private static final Logger LOG = LoggerFactory.getLogger(TabsController.class);
private final MainWindow mainWindow;
private final Map<JNode, TabBlueprint> tabsMap = new HashMap<>();
private final List<ITabStatesListener> listeners = new ArrayList<>();
private boolean forceClose;
private TabBlueprint selectedTab = null;
private @Nullable TabBlueprint selectedTab;
public TabsController(MainWindow mainWindow) {
this.mainWindow = mainWindow;
@@ -57,7 +69,6 @@ public class TabsController {
}
blueprint = newBlueprint;
}
setTabHiddenInternal(blueprint, hidden);
return blueprint;
}
@@ -65,10 +76,68 @@ public class TabsController {
public void selectTab(JNode node) {
TabBlueprint blueprint = openTab(node);
selectedTab = blueprint;
listeners.forEach(l -> l.onTabSelect(blueprint));
}
/**
* Jump to node definition
*/
public void codeJump(JNode node) {
JClass parentCls = node.getJParent();
if (parentCls != null) {
JavaClass cls = node.getJParent().getCls();
JavaClass origTopCls = cls.getOriginalTopParentClass();
JavaClass codeParent = cls.getTopParentClass();
if (!Objects.equals(codeParent, origTopCls)) {
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
jumpCls::loadNode, // load code in background
status -> {
// search original node in jump class
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode();
if (declNode.equals(node.getJavaNode().getCodeNodeRef())) {
codeJump(new JumpPosition(jumpCls, pos));
return true;
}
}
return null;
});
});
return;
}
}
// Not an inline node, jump normally
if (node.getPos() != 0 || node.getRootClass() == null) {
codeJump(new JumpPosition(node));
return;
}
// node need loading
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
() -> node.getRootClass().getCodeInfo(), // run heavy loading in background
status -> codeJump(new JumpPosition(node)));
}
/**
* Prefer {@link TabsController#codeJump(JNode)} method
*/
public void codeJump(JumpPosition pos) {
if (selectedTab == null) {
LOG.warn("Cannot codeJump because selectedTab is null");
return;
}
listeners.forEach(l -> l.onTabCodeJump(selectedTab, pos));
}
public void smaliJump(JClass cls, int pos, boolean debugMode) {
TabBlueprint blueprint = openTab(cls);
listeners.forEach(l -> l.onTabSmaliJump(blueprint, pos, debugMode));
}
public void closeTab(JNode node) {
closeTab(node, false);
}
@@ -219,6 +288,11 @@ public class TabsController {
}
public void notifyRestoreEditorViewStateDone() {
if (selectedTab == null && !tabsMap.isEmpty()) {
JNode node = tabsMap.values().iterator().next().getNode();
LOG.warn("No active tab found, select {}", node); // TODO: find the reason of this issue
selectTab(node);
}
listeners.forEach(ITabStatesListener::onTabsRestoreDone);
}
@@ -43,8 +43,7 @@ public class JumpManager {
return pos.equals(current);
}
@Nullable
private JumpPosition getCurrent() {
public @Nullable JumpPosition getCurrent() {
if (currentPos >= 0 && currentPos < list.size()) {
return list.get(currentPos);
}