diff --git a/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java b/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java index f662f50a2..27fd56931 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java @@ -30,6 +30,8 @@ public class TabStateViewAdapter { tvs.setView(new ViewPoint(viewState.getViewPoint())); tvs.setActive(viewState.isActive()); tvs.setPinned(viewState.isPinned()); + tvs.setBookmarked(viewState.isBookmarked()); + tvs.setHidden(viewState.isHidden()); return tvs; } @@ -43,6 +45,8 @@ public class TabStateViewAdapter { EditorViewState viewState = new EditorViewState(node, tvs.getSubPath(), tvs.getCaret(), tvs.getView().toPoint()); viewState.setActive(tvs.isActive()); viewState.setPinned(tvs.isPinned()); + viewState.setBookmarked(tvs.isBookmarked()); + viewState.setHidden(tvs.isHidden()); return viewState; } catch (Exception e) { LOG.error("Failed to load tab state: " + tvs, e); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/data/TabViewState.java b/jadx-gui/src/main/java/jadx/gui/settings/data/TabViewState.java index 1152540ad..7854b86d6 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/data/TabViewState.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/data/TabViewState.java @@ -8,6 +8,8 @@ public class TabViewState { private ViewPoint view; boolean active; boolean pinned; + boolean bookmarked; + boolean hidden; public String getType() { return type; @@ -64,4 +66,20 @@ public class TabViewState { public void setPinned(boolean pinned) { this.pinned = pinned; } + + public boolean isBookmarked() { + return bookmarked; + } + + public void setBookmarked(boolean bookmarked) { + this.bookmarked = bookmarked; + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } } 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 65d3de502..aab77ba49 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -68,7 +68,7 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable return javaNode.getName(); } - public boolean isPinnable() { + public boolean supportsQuickTabs() { return true; } 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 56b7de4e3..1d91d6606 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -141,6 +141,7 @@ import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.ui.popupmenu.RecentProjectsMenuListener; import jadx.gui.ui.tab.QuickTabsTree; import jadx.gui.ui.tab.TabbedPane; +import jadx.gui.ui.tab.TabsController; import jadx.gui.ui.tab.dnd.TabDndController; import jadx.gui.ui.treenodes.StartPageNode; import jadx.gui.ui.treenodes.SummaryNode; @@ -208,6 +209,7 @@ 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; @@ -370,6 +372,7 @@ public class MainWindow extends JFrame { } private void saveProject() { + saveOpenTabs(); if (!project.isSaveFileSelected()) { saveProjectAs(); } else { @@ -563,7 +566,7 @@ public class MainWindow extends JFrame { resetCache(); LogCollector.getInstance().reset(); wrapper.close(); - tabbedPane.closeAllTabs(); + tabsController.forceCloseAllTabs(); UiUtils.resetClipboardOwner(); System.gc(); update(); @@ -1342,7 +1345,8 @@ public class MainWindow extends JFrame { leftPane.add(bottomPane, BorderLayout.PAGE_END); treeSplitPane.setLeftComponent(leftPane); - tabbedPane = new TabbedPane(this); + tabsController = new TabsController(this); + tabbedPane = new TabbedPane(this, tabsController); tabbedPane.setMinimumSize(new Dimension(150, 150)); new TabDndController(tabbedPane, settings); @@ -1512,7 +1516,7 @@ public class MainWindow extends JFrame { } private void saveOpenTabs() { - project.saveOpenTabs(tabbedPane.getEditorViewStates()); + project.saveOpenTabs(tabsController.getEditorViewStates()); } private void restoreOpenTabs(List openTabs) { @@ -1521,8 +1525,9 @@ public class MainWindow extends JFrame { return; } for (EditorViewState viewState : openTabs) { - tabbedPane.restoreEditorViewState(viewState); + tabsController.restoreEditorViewState(viewState); } + tabsController.notifyRestoreEditorViewStateDone(); } private void preLoadOpenTabs(List openTabs) { @@ -1572,6 +1577,10 @@ public class MainWindow extends JFrame { return tabbedPane; } + public TabsController getTabsController() { + return tabsController; + } + public JadxSettings getSettings() { return settings; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index ad703105c..c8bbcadb2 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -147,12 +147,14 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem } @Override - public EditorViewState getEditorViewState() { + public void saveEditorViewState(EditorViewState viewState) { CodePanel codePanel = (CodePanel) areaTabbedPane.getSelectedComponent(); int caretPos = codePanel.getCodeArea().getCaretPosition(); Point viewPoint = codePanel.getCodeScrollPane().getViewport().getViewPosition(); String subPath = codePanel == javaCodePanel ? "java" : "smali"; - return new EditorViewState(getNode(), subPath, caretPos, viewPoint); + viewState.setSubPath(subPath); + viewState.setCaretPos(caretPos); + viewState.setViewPoint(viewPoint); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java index c9f929283..f97083542 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java @@ -56,10 +56,11 @@ public final class CodeContentPanel extends AbstractCodeContentPanel implements } @Override - public EditorViewState getEditorViewState() { + public void saveEditorViewState(EditorViewState viewState) { int caretPos = codePanel.getCodeArea().getCaretPosition(); Point viewPoint = codePanel.getCodeScrollPane().getViewport().getViewPosition(); - return new EditorViewState(getNode(), "", caretPos, viewPoint); + viewState.setCaretPos(caretPos); + viewState.setViewPoint(viewPoint); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/EditorViewState.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/EditorViewState.java index 13bc8f7bb..62d44b3a5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/EditorViewState.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/EditorViewState.java @@ -8,11 +8,19 @@ public class EditorViewState { public static final Point ZERO = new Point(0, 0); private final JNode node; - private final int caretPos; - private final Point viewPoint; - private final String subPath; + private int caretPos; + private Point viewPoint; + private String subPath; + private boolean active; + private boolean pinned; + private boolean bookmarked; + private boolean hidden; + + public EditorViewState(JNode node) { + this(node, "", 0, EditorViewState.ZERO); + } public EditorViewState(JNode node, String subPath, int caretPos, Point viewPoint) { this.node = node; @@ -29,14 +37,26 @@ public class EditorViewState { return caretPos; } + public void setCaretPos(int caretPos) { + this.caretPos = caretPos; + } + public Point getViewPoint() { return viewPoint; } + public void setViewPoint(Point viewPoint) { + this.viewPoint = viewPoint; + } + public String getSubPath() { return subPath; } + public void setSubPath(String subPath) { + this.subPath = subPath; + } + public boolean isActive() { return active; } @@ -53,6 +73,22 @@ public class EditorViewState { this.pinned = pinned; } + public boolean isBookmarked() { + return bookmarked; + } + + public void setBookmarked(boolean bookmarked) { + this.bookmarked = bookmarked; + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + @Override public String toString() { return "EditorViewState{node=" + node diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java index d68520e72..2825044b5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java @@ -15,11 +15,10 @@ public abstract class ContentPanel extends JPanel { protected TabbedPane tabbedPane; protected JNode node; - private boolean pinned; - protected ContentPanel(TabbedPane panel, JNode jnode) { + protected ContentPanel(TabbedPane panel, JNode node) { tabbedPane = panel; - node = jnode; + this.node = node; } public abstract void loadSettings(); @@ -40,27 +39,19 @@ public abstract class ContentPanel extends JPanel { */ @Nullable public String getTabTooltip() { - JClass jClass = node.getRootClass(); + JClass jClass = getNode().getRootClass(); if (jClass != null) { return jClass.getFullName(); } - return node.getName(); + return getNode().getName(); } public JadxSettings getSettings() { return tabbedPane.getMainWindow().getSettings(); } - public boolean isPinned() { - return pinned; - } - - public void setPinned(boolean pinned) { - this.pinned = pinned; - } - - public boolean isPinnable() { - return node.isPinnable(); + public boolean supportsQuickTabs() { + return getNode().supportsQuickTabs(); } public void dispose() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/IViewStateSupport.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/IViewStateSupport.java index 72365b3a5..5acd433a4 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/IViewStateSupport.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/IViewStateSupport.java @@ -4,7 +4,7 @@ import jadx.gui.ui.codearea.EditorViewState; public interface IViewStateSupport { - EditorViewState getEditorViewState(); + void saveEditorViewState(EditorViewState viewState); void restoreEditorViewState(EditorViewState viewState); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java index aa48ccafe..805fa2c92 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java @@ -1,7 +1,29 @@ package jadx.gui.ui.tab; -import jadx.gui.treemodel.JNode; +import java.util.ArrayList; + +import jadx.gui.ui.codearea.EditorViewState; public interface ITabStatesListener { - void onTabPinChange(JNode node, boolean pinned); + void onTabOpen(TabBlueprint blueprint); + + void onTabSelect(TabBlueprint blueprint); + + void onTabClose(TabBlueprint blueprint); + + void onTabPositionFirst(TabBlueprint blueprint); + + void onTabPinChange(TabBlueprint blueprint); + + void onTabBookmarkChange(TabBlueprint blueprint); + + void onTabVisibilityChange(TabBlueprint blueprint); + + void onTabRestore(TabBlueprint blueprint, EditorViewState viewState); + + void onTabsRestoreDone(); + + void onTabsReorder(ArrayList blueprints); + + void onTabSave(TabBlueprint blueprint, EditorViewState viewState); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBookmarkParentNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBookmarkParentNode.java new file mode 100644 index 000000000..48f87ea77 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBookmarkParentNode.java @@ -0,0 +1,39 @@ +package jadx.gui.ui.tab; + +import javax.swing.Icon; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.Icons; +import jadx.gui.utils.NLS; + +public class QuickTabsBookmarkParentNode extends QuickTabsParentNode { + protected QuickTabsBookmarkParentNode(TabsController tabsController) { + super(tabsController); + } + + @Override + public String getTitle() { + return NLS.str("tree.bookmarked_tabs"); + } + + @Override + Icon getIcon() { + return Icons.BOOKMARK_DARK; + } + + @Override + JPopupMenu onTreePopupMenu(MainWindow mainWindow) { + if (getChildCount() == 0) { + return null; + } + + JPopupMenu menu = new JPopupMenu(); + JMenuItem unbookmarkAll = new JMenuItem(NLS.str("tabs.unbookmark_all")); + unbookmarkAll.addActionListener(e -> getTabsController().unbookmarkAllTabs()); + menu.add(unbookmarkAll); + + return menu; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java index c6461afad..4af101ea9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java @@ -28,20 +28,37 @@ public class QuickTabsChildNode extends QuickTabsBaseNode { public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { JPopupMenu menu = node.onTreePopupMenu(mainWindow); - if (node.isPinnable()) { - if (menu == null) { - menu = new JPopupMenu(); - } - - JMenuItem unpinAction = new JMenuItem(NLS.str("tabs.unpin")); - unpinAction.addActionListener(e -> { - TabComponent tabComponent = mainWindow.getTabbedPane().getTabComponentByNode(node); - if (tabComponent != null) { - tabComponent.togglePin(); + if (node.supportsQuickTabs()) { + if (getParent() instanceof QuickTabsPinParentNode) { + if (menu == null) { + menu = new JPopupMenu(); } - }); - menu.add(unpinAction, 0); - menu.add(new JPopupMenu.Separator(), 1); + + JMenuItem closeAction = new JMenuItem(NLS.str("tabs.close")); + closeAction.addActionListener(e -> mainWindow.getTabsController().closeTab(node, true)); + menu.add(closeAction, 0); + menu.add(new JPopupMenu.Separator(), 1); + } + if (getParent() instanceof QuickTabsPinParentNode) { + if (menu == null) { + menu = new JPopupMenu(); + } + + JMenuItem unpinAction = new JMenuItem(NLS.str("tabs.unpin")); + unpinAction.addActionListener(e -> mainWindow.getTabsController().setTabPinned(node, false)); + menu.add(unpinAction, 0); + menu.add(new JPopupMenu.Separator(), 1); + } + if (getParent() instanceof QuickTabsBookmarkParentNode) { + if (menu == null) { + menu = new JPopupMenu(); + } + + JMenuItem unbookmarkAction = new JMenuItem(NLS.str("tabs.unbookmark")); + unbookmarkAction.addActionListener(e -> mainWindow.getTabsController().setTabBookmarked(node, false)); + menu.add(unbookmarkAction, 0); + menu.add(new JPopupMenu.Separator(), 1); + } } return menu; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsOpenParentNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsOpenParentNode.java new file mode 100644 index 000000000..6a03da097 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsOpenParentNode.java @@ -0,0 +1,39 @@ +package jadx.gui.ui.tab; + +import javax.swing.Icon; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.Icons; +import jadx.gui.utils.NLS; + +public class QuickTabsOpenParentNode extends QuickTabsParentNode { + protected QuickTabsOpenParentNode(TabsController tabsController) { + super(tabsController); + } + + @Override + public String getTitle() { + return NLS.str("tree.open_tabs"); + } + + @Override + Icon getIcon() { + return Icons.FOLDER; + } + + @Override + JPopupMenu onTreePopupMenu(MainWindow mainWindow) { + if (getChildCount() == 0) { + return null; + } + + JPopupMenu menu = new JPopupMenu(); + JMenuItem closeAll = new JMenuItem(NLS.str("tabs.closeAll")); + closeAll.addActionListener(e -> getTabsController().closeAllTabs(true)); + menu.add(closeAll); + + return menu; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java index 19ac93622..5fe5e9e65 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java @@ -9,38 +9,44 @@ import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; abstract class QuickTabsParentNode extends QuickTabsBaseNode { - private final TabbedPane tabbedPane; + private final TabsController tabsController; private final Map childrenMap = new HashMap<>(); - protected QuickTabsParentNode(TabbedPane tabbedPane) { + protected QuickTabsParentNode(TabsController tabsController) { super(); - this.tabbedPane = tabbedPane; + this.tabsController = tabsController; } - public void addJNode(JNode node) { + public boolean addJNode(JNode node) { if (childrenMap.containsKey(node)) { - return; + return false; } QuickTabsChildNode childNode = new QuickTabsChildNode(node); childrenMap.put(node, childNode); add(childNode); + return true; } - public void removeJNode(JNode node) { + public boolean removeJNode(JNode node) { QuickTabsChildNode childNode = childrenMap.remove(node); if (childNode == null) { - return; + return false; } remove(childNode); + return true; + } + + public void removeAllNodes() { + removeAllChildren(); } public QuickTabsChildNode getQuickTabsNode(JNode node) { return childrenMap.get(node); } - public TabbedPane getTabbedPane() { - return tabbedPane; + public TabsController getTabsController() { + return tabsController; } abstract String getTitle(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.java index b5f479e3a..7ffd0af90 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.java @@ -9,8 +9,8 @@ import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class QuickTabsPinParentNode extends QuickTabsParentNode { - protected QuickTabsPinParentNode(TabbedPane tabbedPane) { - super(tabbedPane); + protected QuickTabsPinParentNode(TabsController tabsController) { + super(tabsController); } @Override @@ -31,7 +31,7 @@ public class QuickTabsPinParentNode extends QuickTabsParentNode { JPopupMenu menu = new JPopupMenu(); JMenuItem unpinAll = new JMenuItem(NLS.str("tabs.unpin_all")); - unpinAll.addActionListener(e -> getTabbedPane().unpinAll()); + unpinAll.addActionListener(e -> getTabsController().unpinAllTabs()); menu.add(unpinAll); return menu; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java index 456da8127..8cd63de1f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java @@ -6,6 +6,7 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.ArrayList; import javax.swing.JPopupMenu; import javax.swing.JTree; @@ -19,23 +20,29 @@ 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 { private final MainWindow mainWindow; private final DefaultTreeModel treeModel; + private final QuickTabsParentNode openParentNode; private final QuickTabsParentNode pinParentNode; + private final QuickTabsParentNode bookmarkParentNode; public QuickTabsTree(MainWindow mainWindow) { this.mainWindow = mainWindow; - mainWindow.getTabbedPane().addTabStateListener(this); + mainWindow.getTabsController().addListener(this); Root root = new Root(); - pinParentNode = new QuickTabsPinParentNode(mainWindow.getTabbedPane()); - fillPinParentNode(); + pinParentNode = new QuickTabsPinParentNode(mainWindow.getTabsController()); + openParentNode = new QuickTabsOpenParentNode(mainWindow.getTabsController()); + bookmarkParentNode = new QuickTabsBookmarkParentNode(mainWindow.getTabsController()); + root.add(openParentNode); root.add(pinParentNode); + root.add(bookmarkParentNode); treeModel = new DefaultTreeModel(root); setModel(treeModel); @@ -69,6 +76,10 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele }); loadSettings(); + + fillOpenParentNode(); + fillPinParentNode(); + fillBookmarkParentNode(); } private void triggerRightClickAction(MouseEvent e) { @@ -91,14 +102,50 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele if (pressedNode instanceof QuickTabsChildNode) { QuickTabsChildNode childNode = (QuickTabsChildNode) pressedNode; - return mainWindow.getTabbedPane().showNode(childNode.getJNode()); + mainWindow.getTabsController().selectTab(childNode.getJNode()); + return true; } return false; } + private void fillOpenParentNode() { + mainWindow.getTabsController().getOpenTabs().forEach(this::onTabOpen); + } + private void fillPinParentNode() { - mainWindow.getTabbedPane().getPinnedTabs().forEach((contentPanel) -> pinParentNode.addJNode(contentPanel.getNode())); + mainWindow.getTabsController().getPinnedTabs().forEach(this::onTabPinChange); + } + + private void fillBookmarkParentNode() { + mainWindow.getTabsController().getBookmarkedTabs().forEach(this::onTabBookmarkChange); + } + + private void clearParentNode(QuickTabsParentNode parentNode) { + int[] childIndices = new int[parentNode.getChildCount()]; + Object[] objects = new Object[parentNode.getChildCount()]; + for (int i = 0; i < childIndices.length; i++) { + childIndices[i] = i; + objects[i] = parentNode.getChildAt(i); + } + parentNode.removeAllNodes(); + treeModel.nodesWereRemoved(parentNode, childIndices, objects); + } + + private void addJNode(QuickTabsParentNode parentNode, JNode node) { + if (parentNode.addJNode(node)) { + treeModel.nodesWereInserted(parentNode, new int[] { parentNode.getChildCount() - 1 }); + } + } + + private void removeJNode(QuickTabsParentNode parentNode, JNode node) { + QuickTabsChildNode child = parentNode.getQuickTabsNode(node); + if (child != null) { + int removedIndex = parentNode.getIndex(child); + if (parentNode.removeJNode(node)) { + treeModel.nodesWereRemoved(parentNode, new int[] { removedIndex }, new Object[] { child }); + } + } } @Override @@ -108,22 +155,10 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele if (selectedNode instanceof QuickTabsChildNode) { QuickTabsChildNode childNode = (QuickTabsChildNode) selectedNode; JNode jNode = childNode.getJNode(); - TabbedPane tabbedPane = mainWindow.getTabbedPane(); - tabbedPane.selectTab(tabbedPane.getTabByNode(jNode)); - } - } - } - @Override - public void onTabPinChange(JNode node, boolean pinned) { - if (pinned) { - pinParentNode.addJNode(node); - treeModel.nodesWereInserted(pinParentNode, new int[] { pinParentNode.getChildCount() - 1 }); - } else { - QuickTabsChildNode child = pinParentNode.getQuickTabsNode(node); - int removedIndex = pinParentNode.getIndex(child); - pinParentNode.removeJNode(node); - treeModel.nodesWereRemoved(pinParentNode, new int[] { removedIndex }, new Object[] { child }); + TabsController tabsController = mainWindow.getTabsController(); + tabsController.selectTab(jNode); + } } } @@ -135,7 +170,81 @@ public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSele } public void dispose() { - mainWindow.getTabbedPane().removeTabStateListener(this); + mainWindow.getTabsController().removeListener(this); + } + + @Override + public void onTabOpen(TabBlueprint blueprint) { + if (!blueprint.isHidden() && blueprint.getNode().supportsQuickTabs()) { + addJNode(openParentNode, blueprint.getNode()); + } + } + + @Override + public void onTabSelect(TabBlueprint blueprint) { + + } + + @Override + public void onTabClose(TabBlueprint blueprint) { + removeJNode(openParentNode, blueprint.getNode()); + removeJNode(pinParentNode, blueprint.getNode()); + removeJNode(bookmarkParentNode, blueprint.getNode()); + } + + @Override + public void onTabPositionFirst(TabBlueprint blueprint) { + + } + + @Override + public void onTabPinChange(TabBlueprint blueprint) { + JNode node = blueprint.getNode(); + if (blueprint.isPinned()) { + addJNode(pinParentNode, node); + } else { + removeJNode(pinParentNode, node); + } + } + + @Override + public void onTabBookmarkChange(TabBlueprint blueprint) { + JNode node = blueprint.getNode(); + if (blueprint.isBookmarked()) { + addJNode(bookmarkParentNode, node); + } else { + removeJNode(bookmarkParentNode, node); + } + } + + @Override + public void onTabVisibilityChange(TabBlueprint blueprint) { + JNode node = blueprint.getNode(); + if (!blueprint.isHidden()) { + addJNode(openParentNode, node); + } else { + removeJNode(openParentNode, node); + } + } + + @Override + public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) { + + } + + @Override + public void onTabsRestoreDone() { + + } + + @Override + public void onTabsReorder(ArrayList blueprints) { + + } + + @Override + public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) { + } private class Root extends DefaultMutableTreeNode { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabBlueprint.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabBlueprint.java new file mode 100644 index 000000000..6059c48bd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabBlueprint.java @@ -0,0 +1,50 @@ +package jadx.gui.ui.tab; + +import jadx.gui.treemodel.JNode; + +public class TabBlueprint { + private final JNode node; + private boolean pinned; + private boolean bookmarked; + private boolean hidden; + + public TabBlueprint(JNode node) { + this.node = node; + } + + public JNode getNode() { + return node; + } + + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + public boolean isBookmarked() { + return bookmarked; + } + + public void setBookmarked(boolean bookmarked) { + this.bookmarked = bookmarked; + } + + public boolean supportsQuickTabs() { + return node.supportsQuickTabs(); + } + + public boolean isReferenced() { + return isBookmarked(); + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java index 13a796a63..6a90f4931 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java @@ -21,6 +21,7 @@ import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicButtonUI; import jadx.core.utils.ListUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; import jadx.gui.treemodel.JNode; @@ -28,6 +29,7 @@ import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.dnd.TabDndGestureListener; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; +import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; @@ -35,14 +37,17 @@ public class TabComponent extends JPanel { private static final long serialVersionUID = -8147035487543610321L; private final TabbedPane tabbedPane; + private final TabsController tabsController; private final ContentPanel contentPanel; + private OverlayIcon icon; private JLabel label; private JButton pinBtn; private JButton closeBtn; public TabComponent(TabbedPane tabbedPane, ContentPanel contentPanel) { this.tabbedPane = tabbedPane; + this.tabsController = tabbedPane.getMainWindow().getTabsController(); this.contentPanel = contentPanel; init(); @@ -56,14 +61,16 @@ public class TabComponent extends JPanel { } private Font getLabelFont() { - return tabbedPane.getMainWindow().getSettings().getFont().deriveFont(Font.BOLD); + return tabsController.getMainWindow().getSettings().getFont().deriveFont(Font.BOLD); } private void init() { setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); setOpaque(false); - JNode node = contentPanel.getNode(); + JNode node = getNode(); + icon = new OverlayIcon(node.getIcon()); + label = new NodeLabel(buildTabTitle(node), node.disableHtml()); label.setFont(getLabelFont()); String toolTip = contentPanel.getTabTooltip(); @@ -71,7 +78,8 @@ public class TabComponent extends JPanel { setToolTipText(toolTip); } label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); - label.setIcon(node.getIcon()); + label.setIcon(icon); + updateBookmarkIcon(); if (node instanceof JEditableNode) { ((JEditableNode) node).addChangeListener(c -> label.setText(buildTabTitle(node))); } @@ -97,20 +105,20 @@ public class TabComponent extends JPanel { closeBtn.setFocusable(false); closeBtn.setBorder(null); closeBtn.setBorderPainted(false); - closeBtn.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel, true)); + closeBtn.addActionListener(e -> { + tabsController.closeTab(node, true); + }); MouseAdapter clickAdapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (SwingUtilities.isMiddleMouseButton(e)) { - tabbedPane.closeCodePanel(contentPanel, true); + tabsController.closeTab(node, true); } else if (SwingUtilities.isRightMouseButton(e)) { - JPopupMenu menu = createTabPopupMenu(contentPanel); + JPopupMenu menu = createTabPopupMenu(); menu.show(e.getComponent(), e.getX(), e.getY()); } else if (SwingUtilities.isLeftMouseButton(e)) { - if (tabbedPane.getSelectedComponent() != contentPanel) { - tabbedPane.selectTab(contentPanel); - } + tabsController.selectTab(node); } } }; @@ -123,15 +131,14 @@ public class TabComponent extends JPanel { } public void updateCloseOrPinButton() { - if (contentPanel.isPinned()) { + if (getBlueprint().isPinned()) { if (closeBtn.isShowing()) { remove(closeBtn); } if (!pinBtn.isShowing()) { add(pinBtn); } - } - if (!contentPanel.isPinned()) { + } else { if (pinBtn.isShowing()) { remove(pinBtn); } @@ -141,13 +148,27 @@ public class TabComponent extends JPanel { } } - public void togglePin() { - contentPanel.setPinned(!contentPanel.isPinned()); - updateCloseOrPinButton(); - if (contentPanel.isPinned()) { - tabbedPane.advanceTab(this); + public void updateBookmarkIcon() { + icon.clear(); + + if (getBlueprint().isBookmarked()) { + icon.add(Icons.BOOKMARK_OVERLAY_DARK); } - tabbedPane.notifyTabStateChange(this, true); + label.repaint(); + } + + private void togglePin() { + boolean pinned = !getBlueprint().isPinned(); + tabsController.setTabPinned(getNode(), pinned); + + if (pinned) { + tabsController.setTabPositionFirst(getNode()); + } + } + + private void toggleBookmark() { + boolean bookmarked = !getBlueprint().isBookmarked(); + tabsController.setTabBookmarked(getNode(), bookmarked); } private void addListenerForDnd() { @@ -180,7 +201,7 @@ public class TabComponent extends JPanel { return tabTitle; } - private JPopupMenu createTabPopupMenu(final ContentPanel contentPanel) { + private JPopupMenu createTabPopupMenu() { JPopupMenu menu = new JPopupMenu(); String nodeFullName = getNodeFullName(contentPanel); @@ -191,51 +212,63 @@ public class TabComponent extends JPanel { menu.addSeparator(); } - if (contentPanel.isPinnable()) { - String pinTitle = contentPanel.isPinned() ? NLS.str("tabs.unpin") : NLS.str("tabs.pin"); + if (getBlueprint().supportsQuickTabs()) { + String pinTitle = getBlueprint().isPinned() ? NLS.str("tabs.unpin") : NLS.str("tabs.pin"); JMenuItem pinTab = new JMenuItem(pinTitle); pinTab.addActionListener(e -> togglePin()); menu.add(pinTab); JMenuItem unpinAll = new JMenuItem(NLS.str("tabs.unpin_all")); - unpinAll.addActionListener(e -> tabbedPane.unpinAll()); + unpinAll.addActionListener(e -> tabsController.unpinAllTabs()); menu.add(unpinAll); + + String bookmarkTitle = getBlueprint().isBookmarked() ? NLS.str("tabs.unbookmark") : NLS.str("tabs.bookmark"); + JMenuItem bookmarkTab = new JMenuItem(bookmarkTitle); + bookmarkTab.addActionListener(e -> toggleBookmark()); + menu.add(bookmarkTab); + + JMenuItem unbookmarkAll = new JMenuItem(NLS.str("tabs.unbookmark_all")); + unbookmarkAll.addActionListener(e -> tabsController.unbookmarkAllTabs()); + menu.add(unbookmarkAll); } JMenuItem closeTab = new JMenuItem(NLS.str("tabs.close")); - closeTab.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel, true)); - if (contentPanel.isPinned()) { + closeTab.addActionListener(e -> tabsController.closeTab(getNode(), true)); + if (getBlueprint().isPinned()) { closeTab.setEnabled(false); } menu.add(closeTab); - List tabs = tabbedPane.getTabs(); + List tabs = tabsController.getOpenTabs(); if (tabs.size() > 1) { JMenuItem closeOther = new JMenuItem(NLS.str("tabs.closeOthers")); closeOther.addActionListener(e -> { - for (ContentPanel panel : tabs) { - if (panel != contentPanel) { - tabbedPane.closeCodePanel(panel, true); + for (TabBlueprint tab : tabs) { + if (tab != getBlueprint()) { + tabsController.closeTab(getNode(), true); } } }); menu.add(closeOther); JMenuItem closeAll = new JMenuItem(NLS.str("tabs.closeAll")); - closeAll.addActionListener(e -> tabbedPane.closeAllTabs(true)); + closeAll.addActionListener(e -> tabsController.closeAllTabs(true)); menu.add(closeAll); - if (contentPanel != ListUtils.last(tabs)) { + // We don't use TabsController here because tabs position is + // specific to TabbedPane + List contentPanels = tabbedPane.getTabs(); + if (contentPanel != ListUtils.last(contentPanels)) { JMenuItem closeAllRight = new JMenuItem(NLS.str("tabs.closeAllRight")); closeAllRight.addActionListener(e -> { boolean pastCurrentPanel = false; - for (ContentPanel panel : tabs) { + for (ContentPanel panel : contentPanels) { if (!pastCurrentPanel) { if (panel == contentPanel) { pastCurrentPanel = true; } } else { - tabbedPane.closeCodePanel(panel, true); + tabsController.closeTab(panel.getNode(), true); } } }); @@ -243,15 +276,15 @@ public class TabComponent extends JPanel { } menu.addSeparator(); - ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel(); - for (ContentPanel tab : tabs) { - if (tab == selectedContentPanel) { + TabBlueprint selectedTab = tabsController.getSelectedTab(); + for (TabBlueprint tab : tabs) { + if (tab == selectedTab) { continue; } JNode node = tab.getNode(); final String clsName = node.makeLongString(); JMenuItem item = new JMenuItem(clsName); - item.addActionListener(e -> tabbedPane.setSelectedComponent(tab)); + item.addActionListener(e -> tabsController.selectTab(node)); item.setIcon(node.getIcon()); menu.add(item); } @@ -271,4 +304,17 @@ public class TabComponent extends JPanel { public ContentPanel getContentPanel() { return contentPanel; } + + public TabBlueprint getBlueprint() { + TabBlueprint blueprint = tabsController.getTabByNode(contentPanel.getNode()); + if (blueprint == null) { + throw new JadxRuntimeException("TabComponent does not have a corresponding TabBlueprint"); + } + + return blueprint; + } + + public JNode getNode() { + return contentPanel.getNode(); + } } 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 b7cef842a..00742a3d7 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 @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; @@ -42,15 +43,15 @@ import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -public class TabbedPane extends JTabbedPane { +public class TabbedPane extends JTabbedPane implements ITabStatesListener { private static final long serialVersionUID = -8833600618794570904L; private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class); private final transient MainWindow mainWindow; + private final transient TabsController controller; private final transient Map tabsMap = new HashMap<>(); - private final ArrayList tabStatesListeners = new ArrayList<>(); private final transient JumpManager jumps = new JumpManager(); private transient ContentPanel curTab; @@ -58,9 +59,11 @@ public class TabbedPane extends JTabbedPane { private transient TabDndController dnd; - public TabbedPane(MainWindow window) { + public TabbedPane(MainWindow window, TabsController controller) { this.mainWindow = window; + this.controller = controller; + controller.addListener(this); setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); addMouseWheelListener(event -> { @@ -299,10 +302,7 @@ public class TabbedPane extends JTabbedPane { } public void selectTab(ContentPanel contentPanel) { - setSelectedComponent(contentPanel); - if (mainWindow.getSettings().isAlwaysSelectOpened()) { - mainWindow.syncWithEditor(); - } + controller.selectTab(contentPanel.getNode()); } public void smaliJump(JClass cls, int pos, boolean debugMode) { @@ -334,44 +334,6 @@ public class TabbedPane extends JTabbedPane { return null; } - public List getEditorViewStates() { - ContentPanel selected = getSelectedContentPanel(); - List states = new ArrayList<>(); - for (ContentPanel panel : getTabs()) { - EditorViewState viewState; - if (panel instanceof IViewStateSupport) { - viewState = ((IViewStateSupport) panel).getEditorViewState(); - } else { - viewState = new EditorViewState(panel.getNode(), "", 0, EditorViewState.ZERO); - } - viewState.setActive(panel == selected); - viewState.setPinned(panel.isPinned()); - states.add(viewState); - } - return states; - } - - public void restoreEditorViewState(EditorViewState viewState) { - ContentPanel contentPanel = getContentPanel(viewState.getNode()); - if (contentPanel instanceof IViewStateSupport) { - ((IViewStateSupport) contentPanel).restoreEditorViewState(viewState); - } - if (viewState.isActive()) { - setSelectedComponent(contentPanel); - } - if (contentPanel != null) { - boolean pinned = viewState.isPinned(); - contentPanel.setPinned(pinned); - Component component = getTabComponentAt(indexOfComponent(contentPanel)); - if (component instanceof TabComponent) { - TabComponent tabComponent = (TabComponent) component; - JNode node = contentPanel.getNode(); - tabComponent.updateCloseOrPinButton(); - tabStatesListeners.forEach(l -> l.onTabPinChange(node, pinned)); - } - } - } - public void navBack() { if (jumps.size() > 1) { jumps.updateCurPosition(getCurrentPosition()); @@ -404,36 +366,7 @@ public class TabbedPane extends JTabbedPane { } public void closeCodePanel(ContentPanel contentPanel, boolean considerPins) { - if (considerPins && contentPanel.isPinned()) { - return; - } - - tabsMap.remove(contentPanel.getNode()); - remove(contentPanel); - contentPanel.dispose(); - } - - public void advanceTab(TabComponent tabComponent) { - remove(tabComponent.getContentPanel()); - add(tabComponent.getContentPanel(), 0); - setTabComponentAt(0, tabComponent); - selectTab(tabComponent.getContentPanel()); - } - - public void notifyTabStateChange(TabComponent tabComponent, boolean pinChange) { - if (pinChange) { - JNode node = tabComponent.getContentPanel().getNode(); - boolean pinned = tabComponent.getContentPanel().isPinned(); - tabStatesListeners.forEach(l -> l.onTabPinChange(node, pinned)); - } - } - - public void addTabStateListener(ITabStatesListener listener) { - tabStatesListeners.add(listener); - } - - public void removeTabStateListener(ITabStatesListener listener) { - tabStatesListeners.remove(listener); + controller.closeTab(contentPanel.getNode(), considerPins); } public List getTabs() { @@ -444,28 +377,6 @@ public class TabbedPane extends JTabbedPane { return list; } - public List getPinnedTabs() { - List list = new ArrayList<>(getTabCount()); - for (int i = 0; i < getTabCount(); i++) { - ContentPanel contentPanel = (ContentPanel) getComponentAt(i); - if (contentPanel.isPinned()) { - list.add(contentPanel); - } - } - return list; - } - - public List getPinnedTabComponents() { - List list = new ArrayList<>(getTabCount()); - for (int i = 0; i < getTabCount(); i++) { - ContentPanel contentPanel = (ContentPanel) getComponentAt(i); - if (contentPanel.isPinned()) { - list.add((TabComponent) getTabComponentAt(i)); - } - } - return list; - } - public @Nullable ContentPanel getTabByNode(JNode node) { return tabsMap.get(node); } @@ -480,21 +391,8 @@ public class TabbedPane extends JTabbedPane { } private @Nullable ContentPanel getContentPanel(JNode node) { - ContentPanel panel = getTabByNode(node); - if (panel != null) { - return panel; - } - ContentPanel newPanel = node.getContentPanel(this); - if (newPanel == null) { - return null; - } - FocusManager.listen(newPanel); - addContentPanel(newPanel); - return newPanel; - } - - public void unpinAll() { - getPinnedTabComponents().forEach(TabComponent::togglePin); + controller.openTab(node); + return getTabByNode(node); } public void refresh(JNode node) { @@ -517,19 +415,18 @@ public class TabbedPane extends JTabbedPane { continue; } ContentPanel oldPanel = (ContentPanel) getComponentAt(i); - EditorViewState viewState = null; - if (oldPanel instanceof IViewStateSupport) { - viewState = ((IViewStateSupport) oldPanel).getEditorViewState(); + TabBlueprint tab = controller.getTabByNode(oldPanel.getNode()); + if (tab == null) { + continue; } + EditorViewState viewState = controller.getEditorViewState(tab); JNode node = oldPanel.getNode(); ContentPanel panel = node.getContentPanel(this); - if (viewState != null && panel instanceof IViewStateSupport) { - ((IViewStateSupport) panel).restoreEditorViewState(viewState); - } FocusManager.listen(panel); tabsMap.put(node, panel); setComponentAt(i, panel); setTabComponentAt(i, makeTabComponent(panel)); + controller.restoreEditorViewState(viewState); } fireStateChanged(); } @@ -582,6 +479,123 @@ public class TabbedPane extends JTabbedPane { this.dnd = dnd; } + @Override + public void onTabOpen(TabBlueprint blueprint) { + if (blueprint.isHidden()) { + return; + } + ContentPanel newPanel = blueprint.getNode().getContentPanel(this); + FocusManager.listen(newPanel); + addContentPanel(newPanel); + } + + @Override + public void onTabSelect(TabBlueprint blueprint) { + ContentPanel contentPanel = getContentPanel(blueprint.getNode()); + setSelectedComponent(contentPanel); + if (mainWindow.getSettings().isAlwaysSelectOpened()) { + mainWindow.syncWithEditor(); + } + } + + @Override + public void onTabClose(TabBlueprint blueprint) { + ContentPanel contentPanel = getTabByNode(blueprint.getNode()); + if (contentPanel == null) { + return; + } + tabsMap.remove(contentPanel.getNode()); + remove(contentPanel); + contentPanel.dispose(); + } + + @Override + public void onTabPositionFirst(TabBlueprint blueprint) { + ContentPanel contentPanel = getTabByNode(blueprint.getNode()); + if (contentPanel == null) { + return; + } + setTabPosition(contentPanel, 0); + } + + private void setTabPosition(ContentPanel contentPanel, int position) { + TabComponent tabComponent = getTabComponentByNode(contentPanel.getNode()); + if (tabComponent == null) { + return; + } + remove(contentPanel); + add(contentPanel, position); + setTabComponentAt(position, tabComponent); + } + + @Override + public void onTabPinChange(TabBlueprint blueprint) { + TabComponent tabComponent = getTabComponentByNode(blueprint.getNode()); + if (tabComponent == null) { + return; + } + tabComponent.updateCloseOrPinButton(); + } + + @Override + public void onTabBookmarkChange(TabBlueprint blueprint) { + TabComponent tabComponent = getTabComponentByNode(blueprint.getNode()); + if (tabComponent == null) { + return; + } + tabComponent.updateBookmarkIcon(); + } + + @Override + public void onTabVisibilityChange(TabBlueprint blueprint) { + if (!blueprint.isHidden() && !tabsMap.containsKey(blueprint.getNode())) { + onTabOpen(blueprint); + } + if (blueprint.isHidden() && tabsMap.containsKey(blueprint.getNode())) { + onTabClose(blueprint); + } + } + + @Override + public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) { + ContentPanel contentPanel = getTabByNode(blueprint.getNode()); + if (contentPanel instanceof IViewStateSupport) { + ((IViewStateSupport) contentPanel).restoreEditorViewState(viewState); + } + } + + @Override + public void onTabsRestoreDone() { + } + + @Override + public void onTabsReorder(ArrayList blueprints) { + ArrayList newBlueprints = new ArrayList<>(); + for (ContentPanel contentPanel : getTabs()) { + Optional blueprintFindResult = blueprints.stream() + .filter(b -> b.getNode() == contentPanel.getNode()) + .findFirst(); + if (blueprintFindResult.isPresent()) { + TabBlueprint blueprint = blueprintFindResult.get(); + blueprints.remove(blueprint); + newBlueprints.add(blueprint); + } + } + // Add back hidden tabs + newBlueprints.addAll(blueprints); + + blueprints.clear(); + blueprints.addAll(newBlueprints); + } + + @Override + public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) { + ContentPanel contentPanel = getTabByNode(blueprint.getNode()); + if (contentPanel instanceof IViewStateSupport) { + ((IViewStateSupport) contentPanel).saveEditorViewState(viewState); + } + } + private static class FocusManager implements FocusListener { private static final FocusManager INSTANCE = new FocusManager(); private static @Nullable Component focusedComp; 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 new file mode 100644 index 000000000..192e7695e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java @@ -0,0 +1,244 @@ +package jadx.gui.ui.tab; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.ui.codearea.EditorViewState; + +public class TabsController { + private final transient MainWindow mainWindow; + private final Map tabsMap = new HashMap<>(); + private final ArrayList listeners = new ArrayList<>(); + + private boolean forceClose; + + private TabBlueprint selectedTab = null; + + public TabsController(MainWindow mainWindow) { + this.mainWindow = mainWindow; + } + + public MainWindow getMainWindow() { + return mainWindow; + } + + public void addListener(ITabStatesListener listener) { + listeners.add(listener); + } + + public void removeListener(ITabStatesListener listener) { + listeners.remove(listener); + } + + public @Nullable TabBlueprint getTabByNode(JNode node) { + return tabsMap.get(node); + } + + public TabBlueprint openTab(JNode node) { + return openTab(node, false); + } + + public TabBlueprint openTab(JNode node, boolean hidden) { + TabBlueprint blueprint = getTabByNode(node); + if (blueprint == null) { + TabBlueprint newBlueprint = new TabBlueprint(node); + tabsMap.put(node, newBlueprint); + newBlueprint.setHidden(hidden); + listeners.forEach(l -> l.onTabOpen(newBlueprint)); + if (hidden) { + listeners.forEach(l -> l.onTabVisibilityChange(newBlueprint)); + } + blueprint = newBlueprint; + } + + setTabHiddenInternal(blueprint, hidden); + return blueprint; + } + + public void selectTab(JNode node) { + TabBlueprint blueprint = openTab(node); + selectedTab = blueprint; + + listeners.forEach(l -> l.onTabSelect(blueprint)); + } + + public void closeTab(JNode node) { + closeTab(node, false); + } + + public void closeTab(JNode node, boolean considerPins) { + TabBlueprint blueprint = getTabByNode(node); + + if (blueprint == null) { + return; + } + + if (forceClose) { + closeTabForce(blueprint); + return; + } + + if (!considerPins || !blueprint.isPinned()) { + if (!blueprint.isReferenced()) { + closeTabForce(blueprint); + } else { + closeTabSoft(blueprint); + } + } + } + + /* + * Removes Tab from everywhere + */ + private void closeTabForce(TabBlueprint blueprint) { + listeners.forEach(l -> l.onTabClose(blueprint)); + tabsMap.remove(blueprint.getNode()); + } + + /* + * Hides Tab from TabbedPane + */ + private void closeTabSoft(TabBlueprint blueprint) { + setTabHidden(blueprint.getNode(), true); + } + + public void setTabPositionFirst(JNode node) { + TabBlueprint blueprint = openTab(node); + listeners.forEach(l -> l.onTabPositionFirst(blueprint)); + } + + public void setTabPinned(JNode node, boolean pinned) { + TabBlueprint blueprint = openTab(node); + setTabPinnedInternal(blueprint, pinned); + } + + public void setTabPinnedInternal(TabBlueprint blueprint, boolean pinned) { + if (blueprint.isPinned() != pinned) { + blueprint.setPinned(pinned); + listeners.forEach(l -> l.onTabPinChange(blueprint)); + } + } + + public void setTabBookmarked(JNode node, boolean bookmarked) { + TabBlueprint blueprint = openTab(node); + setTabBookmarkedInternal(blueprint, bookmarked); + } + + private void setTabBookmarkedInternal(TabBlueprint blueprint, boolean bookmarked) { + if (blueprint.isBookmarked() != bookmarked) { + blueprint.setBookmarked(bookmarked); + listeners.forEach(l -> l.onTabBookmarkChange(blueprint)); + removeTabIfNotReferenced(blueprint); + } + } + + public void setTabHidden(JNode node, boolean hidden) { + TabBlueprint blueprint = getTabByNode(node); + setTabHiddenInternal(blueprint, hidden); + } + + private void setTabHiddenInternal(TabBlueprint blueprint, boolean hidden) { + if (blueprint != null && blueprint.isHidden() != hidden) { + blueprint.setHidden(hidden); + listeners.forEach(l -> l.onTabVisibilityChange(blueprint)); + } + } + + private void removeTabIfNotReferenced(TabBlueprint blueprint) { + if (blueprint.isHidden() && !blueprint.isReferenced()) { + tabsMap.remove(blueprint.getNode()); + } + } + + public void closeAllTabs() { + closeAllTabs(false); + } + + public void forceCloseAllTabs() { + forceClose = true; + closeAllTabs(); + forceClose = false; + } + + public boolean isForceClose() { + return forceClose; + } + + public void closeAllTabs(boolean considerPins) { + List.copyOf(tabsMap.values()).forEach(t -> closeTab(t.getNode(), considerPins)); + } + + public void unpinAllTabs() { + tabsMap.values().forEach(t -> setTabPinned(t.getNode(), false)); + } + + public void unbookmarkAllTabs() { + tabsMap.values().forEach(t -> setTabBookmarked(t.getNode(), false)); + } + + public TabBlueprint getSelectedTab() { + return selectedTab; + } + + public List getTabs() { + return List.copyOf(tabsMap.values()); + } + + public List getOpenTabs() { + return List.copyOf(tabsMap.values()); + } + + public List getPinnedTabs() { + return tabsMap.values().stream() + .filter(TabBlueprint::isPinned) + .collect(Collectors.toUnmodifiableList()); + } + + public List getBookmarkedTabs() { + return tabsMap.values().stream() + .filter(TabBlueprint::isBookmarked) + .collect(Collectors.toUnmodifiableList()); + } + + public void restoreEditorViewState(EditorViewState viewState) { + JNode node = viewState.getNode(); + TabBlueprint blueprint = openTab(node, viewState.isHidden()); + setTabPinnedInternal(blueprint, viewState.isPinned()); + setTabBookmarkedInternal(blueprint, viewState.isBookmarked()); + listeners.forEach(l -> l.onTabRestore(blueprint, viewState)); + if (viewState.isActive()) { + selectTab(node); + } + } + + public void notifyRestoreEditorViewStateDone() { + listeners.forEach(ITabStatesListener::onTabsRestoreDone); + } + + public List getEditorViewStates() { + ArrayList reorderedTabs = new ArrayList<>(tabsMap.values()); + listeners.forEach(l -> l.onTabsReorder(reorderedTabs)); + List states = new ArrayList<>(); + for (TabBlueprint blueprint : reorderedTabs) { + states.add(getEditorViewState(blueprint)); + } + return states; + } + + public EditorViewState getEditorViewState(TabBlueprint blueprint) { + EditorViewState viewState = new EditorViewState(blueprint.getNode()); + listeners.forEach(l -> l.onTabSave(blueprint, viewState)); + viewState.setActive(blueprint == selectedTab); + viewState.setPinned(blueprint.isPinned()); + viewState.setBookmarked(blueprint.isBookmarked()); + viewState.setHidden(blueprint.isHidden()); + return viewState; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/StartPageNode.java b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/StartPageNode.java index 278a1a8fe..c61070893 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/StartPageNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/StartPageNode.java @@ -34,7 +34,7 @@ public class StartPageNode extends JNode { } @Override - public boolean isPinnable() { + public boolean supportsQuickTabs() { return false; } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Icons.java b/jadx-gui/src/main/java/jadx/gui/utils/Icons.java index 86f40b1c2..650c41515 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Icons.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Icons.java @@ -19,6 +19,10 @@ public class Icons { public static final ImageIcon PIN_DARK = UiUtils.openSvgIcon("nodes/pin_dark"); public static final ImageIcon PIN_HOVERED = UiUtils.openSvgIcon("nodes/pinHovered"); public static final ImageIcon PIN_HOVERED_DARK = UiUtils.openSvgIcon("nodes/pinHovered_dark"); + public static final ImageIcon BOOKMARK = UiUtils.openSvgIcon("nodes/bookmark"); + public static final ImageIcon BOOKMARK_OVERLAY = UiUtils.openSvgIcon("nodes/bookmark_overlay"); + public static final ImageIcon BOOKMARK_DARK = UiUtils.openSvgIcon("nodes/bookmark_dark"); + public static final ImageIcon BOOKMARK_OVERLAY_DARK = UiUtils.openSvgIcon("nodes/bookmark_overlay_dark"); public static final ImageIcon STATIC = openSvgIcon("nodes/staticMark"); public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark"); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/OverlayIcon.java b/jadx-gui/src/main/java/jadx/gui/utils/OverlayIcon.java index 43ce62a6a..bdfd16ef3 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/OverlayIcon.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/OverlayIcon.java @@ -54,6 +54,14 @@ public class OverlayIcon implements Icon { icons.add(icon); } + public void remove(Icon icon) { + icons.remove(icon); + } + + public void clear() { + icons.clear(); + } + public List getIcons() { return icons; } 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 5bcbd1e15..9a3d75086 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -60,6 +60,8 @@ tree.sources_title=Quelltexte tree.resources_title=Ressourcen tree.loading=Laden… #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=Laden progress.save_mappings=Zuordnungen exportieren @@ -83,6 +85,9 @@ tabs.close=Schließen tabs.closeOthers=Andere schließen #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Alles schließen tabs.closeAllRight=Schließe alles rechts 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 57ce2a500..922bf2249 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -60,6 +60,8 @@ tree.sources_title=Source code tree.resources_title=Resources tree.loading=Loading... tree.pinned_tabs=Pinned Tabs +tree.open_tabs=Open Tabs +tree.bookmarked_tabs=Bookmarked Tabs progress.load=Loading progress.save_mappings=Saving mappings @@ -83,6 +85,9 @@ tabs.close=Close tabs.closeOthers=Close Others tabs.unpin=Unpin tabs.unpin_all=Unpin All +tabs.bookmark=Bookmark +tabs.unbookmark=Unbookmark +tabs.unbookmark_all=Unbookmark All tabs.pin=Pin tabs.closeAll=Close All tabs.closeAllRight=Close All Right 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 296489637..06391b75d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -60,6 +60,8 @@ tree.sources_title=Código fuente tree.resources_title=Recursos tree.loading=Cargando... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=Cargando #progress.save_mappings= @@ -83,6 +85,9 @@ tabs.close=Cerrar tabs.closeOthers=Cerrar otros #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Cerrar todo tabs.closeAllRight=Cierra todo a la derecha diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 6a197b304..15b32ef15 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -60,6 +60,8 @@ tree.sources_title=Kode Sumber tree.resources_title=Sumber Daya tree.loading=Memuat... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=Memuat progress.save_mappings=Menyimpan pemetaan @@ -83,6 +85,9 @@ tabs.close=Tutup tabs.closeOthers=Tutup yang Lain #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Tutup Semua tabs.closeAllRight=Tutup Semua yang Kanan 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 f3859a6ff..6f51ae66c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -60,6 +60,8 @@ tree.sources_title=소스코드 tree.resources_title=리소스 tree.loading=로딩중... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=로딩중 progress.save_mappings=매핑 내보내는 중 @@ -83,6 +85,9 @@ tabs.close=닫기 tabs.closeOthers=이 탭을 제외하고 닫기 #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=모두 닫기 tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오 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 cc37ad523..00940d852 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -60,6 +60,8 @@ tree.sources_title=Código fonte tree.resources_title=Recursos tree.loading=Carregando... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=Carregando #progress.save_mappings=Saving mappings @@ -83,6 +85,9 @@ tabs.close=Fechar tabs.closeOthers=Fechar outros #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Fechar todos tabs.closeAllRight=Feche tudo à direita diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index cd81390ef..58f449271 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -60,6 +60,8 @@ tree.sources_title=Исходный код tree.resources_title=Ресурсы tree.loading=Загрузка... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=Загрузка progress.save_mappings=Сохранить маппинги @@ -83,6 +85,9 @@ tabs.close=Закрыть tabs.closeOthers=Закрыть другие #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Закрыть все tabs.closeAllRight=Закройте все справа 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 25f6df82c..c40ad83c4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -60,6 +60,8 @@ tree.sources_title=源代码 tree.resources_title=资源文件 tree.loading=加载中… #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=正在加载 progress.save_mappings=导出映射 @@ -83,6 +85,9 @@ tabs.close=关闭 tabs.closeOthers=关闭其他 #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=关闭全部 tabs.closeAllRight=关闭右边的所有 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 912c3618c..6381f386b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -60,6 +60,8 @@ tree.sources_title=原始碼 tree.resources_title=資源 tree.loading=載入中... #tree.pinned_tabs=Pinned Tabs +#tree.open_tabs=Open Tabs +#tree.bookmarked_tabs=Bookmarked Tabs progress.load=載入中 progress.save_mappings=正在匯出對應 @@ -83,6 +85,9 @@ tabs.close=關閉 tabs.closeOthers=關閉其他 #tabs.unpin=Unpin #tabs.unpin_all=Unpin All +#tabs.bookmark=Bookmark +#tabs.unbookmark=Unbookmark +#tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=關閉全部 tabs.closeAllRight=關閉右邊的所有 diff --git a/jadx-gui/src/main/resources/icons/nodes/bookmark.svg b/jadx-gui/src/main/resources/icons/nodes/bookmark.svg new file mode 100644 index 000000000..37ddd8909 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/bookmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/bookmark_dark.svg b/jadx-gui/src/main/resources/icons/nodes/bookmark_dark.svg new file mode 100644 index 000000000..829d176c5 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/bookmark_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay.svg b/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay.svg new file mode 100644 index 000000000..206f0b6bb --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay_dark.svg b/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay_dark.svg new file mode 100644 index 000000000..ad56a41c3 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/bookmark_overlay_dark.svg @@ -0,0 +1,4 @@ + + + +