diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 2df0674a4..ad34643c8 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -136,6 +136,8 @@ public class JadxSettings extends JadxCLIArgs { private boolean dockLogViewer = true; + private boolean dockQuickTabs = false; + private TabDndGhostType tabDndGhostType = TabDndGhostType.OUTLINE; private int settingsVersion = CURRENT_SETTINGS_VERSION; @@ -742,7 +744,16 @@ public class JadxSettings extends JadxCLIArgs { public void setDockLogViewer(boolean dockLogViewer) { this.dockLogViewer = dockLogViewer; - partialSync(settings -> this.dockLogViewer = dockLogViewer); + partialSync(settings -> settings.dockLogViewer = dockLogViewer); + } + + public boolean isDockQuickTabs() { + return dockQuickTabs; + } + + public void setDockQuickTabs(boolean dockQuickTabs) { + this.dockQuickTabs = dockQuickTabs; + partialSync(settings -> settings.dockQuickTabs = dockQuickTabs); } public XposedCodegenLanguage getXposedCodegenLanguage() { 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 5e047acc6..f662f50a2 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java @@ -29,6 +29,7 @@ public class TabStateViewAdapter { tvs.setCaret(viewState.getCaretPos()); tvs.setView(new ViewPoint(viewState.getViewPoint())); tvs.setActive(viewState.isActive()); + tvs.setPinned(viewState.isPinned()); return tvs; } @@ -41,6 +42,7 @@ public class TabStateViewAdapter { } EditorViewState viewState = new EditorViewState(node, tvs.getSubPath(), tvs.getCaret(), tvs.getView().toPoint()); viewState.setActive(tvs.isActive()); + viewState.setPinned(tvs.isPinned()); 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 1ca10954d..1152540ad 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 @@ -7,6 +7,7 @@ public class TabViewState { private int caret; private ViewPoint view; boolean active; + boolean pinned; public String getType() { return type; @@ -55,4 +56,12 @@ public class TabViewState { public void setActive(boolean active) { this.active = active; } + + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } } 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 814a4ec61..65d3de502 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -68,6 +68,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable return javaNode.getName(); } + public boolean isPinnable() { + return true; + } + public @Nullable JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return null; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 1dfb87ac6..56b7de4e3 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -7,7 +7,6 @@ import java.awt.DisplayMode; import java.awt.Font; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; -import java.awt.Rectangle; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; @@ -140,6 +139,7 @@ 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.QuickTabsTree; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.ui.tab.dnd.TabDndController; import jadx.gui.ui.treenodes.StartPageNode; @@ -203,6 +203,7 @@ public class MainWindow extends JFrame { private transient JSplitPane treeSplitPane; private transient JSplitPane rightSplitPane; private transient JSplitPane bottomSplitPane; + private transient JSplitPane quickTabsAndCodeSplitPane; private JTree tree; private DefaultTreeModel treeModel; @@ -228,6 +229,7 @@ public class MainWindow extends JFrame { private transient IssuesPanel issuesPanel; private transient @Nullable LogPanel logPanel; private transient @Nullable JDebuggerPanel debuggerPanel; + private transient @Nullable QuickTabsTree quickTabsTree; private final List loadListeners = new ArrayList<>(); private final List> treeUpdateListener = new ArrayList<>(); @@ -888,27 +890,11 @@ public class MainWindow extends JFrame { @Nullable private JNode getJNodeUnderMouse(MouseEvent mouseEvent) { - TreePath path = tree.getClosestPathForLocation(mouseEvent.getX(), mouseEvent.getY()); - if (path == null) { - return null; - } - // allow 'closest' path only at the right of the item row - Rectangle pathBounds = tree.getPathBounds(path); - if (pathBounds != null) { - int y = mouseEvent.getY(); - if (y < pathBounds.y || y > (pathBounds.y + pathBounds.height)) { - return null; - } - if (mouseEvent.getX() < pathBounds.x) { - // exclude expand/collapse events - return null; - } - } - Object obj = path.getLastPathComponent(); - if (obj instanceof JNode) { - tree.setSelectionPath(path); - return (JNode) obj; + TreeNode treeNode = UiUtils.getTreeNodeUnderMouse(tree, mouseEvent); + if (treeNode instanceof JNode) { + return (JNode) treeNode; } + return null; } @@ -1061,6 +1047,17 @@ public class MainWindow extends JFrame { dockLog.setState(settings.isDockLogViewer()); dockLog.addActionListener(event -> settings.setDockLogViewer(!settings.isDockLogViewer())); + JCheckBoxMenuItem dockQuickTabs = new JCheckBoxMenuItem(NLS.str("menu.dock_quick_tabs")); + dockQuickTabs.setState(settings.isDockQuickTabs()); + dockQuickTabs.addActionListener(event -> { + boolean visible = quickTabsTree == null; + setQuickTabsVisibility(visible); + settings.setDockQuickTabs(visible); + }); + if (dockQuickTabs.getState()) { + setQuickTabsVisibility(true); + } + JadxGuiAction syncAction = new JadxGuiAction(ActionModel.SYNC, this::syncWithEditor); JadxGuiAction textSearchAction = new JadxGuiAction(ActionModel.TEXT_SEARCH, this::textSearch); JadxGuiAction clsSearchAction = new JadxGuiAction(ActionModel.CLASS_SEARCH, @@ -1124,6 +1121,7 @@ public class MainWindow extends JFrame { view.add(heapUsageBarMenuItem); view.add(alwaysSelectOpened); view.add(dockLog); + view.add(dockQuickTabs); JMenu nav = new JadxMenu(NLS.str("menu.navigation"), shortcutsController); nav.setMnemonic(KeyEvent.VK_N); @@ -1348,8 +1346,13 @@ public class MainWindow extends JFrame { tabbedPane.setMinimumSize(new Dimension(150, 150)); new TabDndController(tabbedPane, settings); + quickTabsAndCodeSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + quickTabsAndCodeSplitPane.setResizeWeight(0.15); + quickTabsAndCodeSplitPane.setDividerSize(0); + quickTabsAndCodeSplitPane.setRightComponent(tabbedPane); + rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - rightSplitPane.setTopComponent(tabbedPane); + rightSplitPane.setTopComponent(quickTabsAndCodeSplitPane); rightSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); treeSplitPane.setRightComponent(rightSplitPane); @@ -1482,6 +1485,9 @@ public class MainWindow extends JFrame { if (logPanel != null) { logPanel.loadSettings(); } + if (quickTabsTree != null) { + quickTabsTree.loadSettings(); + } shortcutsController.loadSettings(); } @@ -1654,6 +1660,25 @@ public class MainWindow extends JFrame { rightSplitPane.setBottomComponent(null); } + private void setQuickTabsVisibility(boolean visible) { + if (visible) { + if (quickTabsTree == null) { + quickTabsTree = new QuickTabsTree(this); + } + + quickTabsAndCodeSplitPane.setLeftComponent(quickTabsTree); + quickTabsAndCodeSplitPane.setDividerSize(5); + } else { + quickTabsAndCodeSplitPane.setLeftComponent(null); + quickTabsAndCodeSplitPane.setDividerSize(0); + + if (quickTabsTree != null) { + quickTabsTree.dispose(); + quickTabsTree = null; + } + } + } + public JMenu getPluginsMenu() { return pluginsMenu; } 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 9af301bfd..13bc8f7bb 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 @@ -12,6 +12,7 @@ public class EditorViewState { private final Point viewPoint; private final String subPath; private boolean active; + private boolean pinned; public EditorViewState(JNode node, String subPath, int caretPos, Point viewPoint) { this.node = node; @@ -44,6 +45,14 @@ public class EditorViewState { this.active = active; } + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + @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 34261deff..d68520e72 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,6 +15,7 @@ public abstract class ContentPanel extends JPanel { protected TabbedPane tabbedPane; protected JNode node; + private boolean pinned; protected ContentPanel(TabbedPane panel, JNode jnode) { tabbedPane = panel; @@ -50,6 +51,18 @@ public abstract class ContentPanel extends JPanel { return tabbedPane.getMainWindow().getSettings(); } + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + public boolean isPinnable() { + return node.isPinnable(); + } + public void dispose() { tabbedPane = null; node = null; 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 new file mode 100644 index 000000000..aa48ccafe --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java @@ -0,0 +1,7 @@ +package jadx.gui.ui.tab; + +import jadx.gui.treemodel.JNode; + +public interface ITabStatesListener { + void onTabPinChange(JNode node, boolean pinned); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBaseNode.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBaseNode.java new file mode 100644 index 000000000..9303a2f9b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBaseNode.java @@ -0,0 +1,17 @@ +package jadx.gui.ui.tab; + +import javax.swing.Icon; +import javax.swing.JPopupMenu; +import javax.swing.tree.DefaultMutableTreeNode; + +import jadx.gui.ui.MainWindow; + +abstract class QuickTabsBaseNode extends DefaultMutableTreeNode { + JPopupMenu onTreePopupMenu(MainWindow mainWindow) { + return null; + } + + Icon getIcon() { + return null; + } +} 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 new file mode 100644 index 000000000..c6461afad --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java @@ -0,0 +1,54 @@ +package jadx.gui.ui.tab; + +import javax.swing.Icon; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; + +public class QuickTabsChildNode extends QuickTabsBaseNode { + private final JNode node; + + public QuickTabsChildNode(JNode node) { + this.node = node; + } + + @Override + public String toString() { + return node.toString(); + } + + public JNode getJNode() { + return node; + } + + @Override + 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(); + } + }); + menu.add(unpinAction, 0); + menu.add(new JPopupMenu.Separator(), 1); + } + + return menu; + } + + @Override + Icon getIcon() { + return node.getIcon(); + } +} 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 new file mode 100644 index 000000000..19ac93622 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java @@ -0,0 +1,57 @@ +package jadx.gui.ui.tab; + +import java.util.HashMap; +import java.util.Map; + +import javax.swing.JPopupMenu; + +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; + +abstract class QuickTabsParentNode extends QuickTabsBaseNode { + private final TabbedPane tabbedPane; + private final Map childrenMap = new HashMap<>(); + + protected QuickTabsParentNode(TabbedPane tabbedPane) { + super(); + + this.tabbedPane = tabbedPane; + } + + public void addJNode(JNode node) { + if (childrenMap.containsKey(node)) { + return; + } + QuickTabsChildNode childNode = new QuickTabsChildNode(node); + childrenMap.put(node, childNode); + add(childNode); + } + + public void removeJNode(JNode node) { + QuickTabsChildNode childNode = childrenMap.remove(node); + if (childNode == null) { + return; + } + remove(childNode); + } + + public QuickTabsChildNode getQuickTabsNode(JNode node) { + return childrenMap.get(node); + } + + public TabbedPane getTabbedPane() { + return tabbedPane; + } + + abstract String getTitle(); + + @Override + public String toString() { + return getTitle(); + } + + @Override + JPopupMenu onTreePopupMenu(MainWindow mainWindow) { + return super.onTreePopupMenu(mainWindow); + } +} 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 new file mode 100644 index 000000000..b5f479e3a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.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 QuickTabsPinParentNode extends QuickTabsParentNode { + protected QuickTabsPinParentNode(TabbedPane tabbedPane) { + super(tabbedPane); + } + + @Override + public String getTitle() { + return NLS.str("tree.pinned_tabs"); + } + + @Override + Icon getIcon() { + return Icons.PIN; + } + + @Override + JPopupMenu onTreePopupMenu(MainWindow mainWindow) { + if (getChildCount() == 0) { + return null; + } + + JPopupMenu menu = new JPopupMenu(); + JMenuItem unpinAll = new JMenuItem(NLS.str("tabs.unpin_all")); + unpinAll.addActionListener(e -> getTabbedPane().unpinAll()); + 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 new file mode 100644 index 000000000..456da8127 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java @@ -0,0 +1,157 @@ +package jadx.gui.ui.tab; + +import java.awt.Component; +import java.awt.Font; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JPopupMenu; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; + +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.UiUtils; + +public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSelectionListener { + private final MainWindow mainWindow; + private final DefaultTreeModel treeModel; + + private final QuickTabsParentNode pinParentNode; + + public QuickTabsTree(MainWindow mainWindow) { + this.mainWindow = mainWindow; + + mainWindow.getTabbedPane().addTabStateListener(this); + + Root root = new Root(); + pinParentNode = new QuickTabsPinParentNode(mainWindow.getTabbedPane()); + fillPinParentNode(); + root.add(pinParentNode); + + treeModel = new DefaultTreeModel(root); + setModel(treeModel); + setCellRenderer(new CellRenderer()); + setRootVisible(false); + setShowsRootHandles(true); + + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + TreeNode pressedNode = UiUtils.getTreeNodeUnderMouse(QuickTabsTree.this, e); + if (SwingUtilities.isLeftMouseButton(e)) { + if (nodeClickAction(pressedNode)) { + setFocusable(true); + requestFocus(); + } + } + if (SwingUtilities.isRightMouseButton(e)) { + triggerRightClickAction(e); + } + } + }); + + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + nodeClickAction(getLastSelectedPathComponent()); + } + } + }); + + loadSettings(); + } + + private void triggerRightClickAction(MouseEvent e) { + TreeNode treeNode = UiUtils.getTreeNodeUnderMouse(this, e); + if (!(treeNode instanceof QuickTabsBaseNode)) { + return; + } + + QuickTabsBaseNode quickTabsNode = (QuickTabsBaseNode) treeNode; + JPopupMenu menu = quickTabsNode.onTreePopupMenu(mainWindow); + if (menu != null) { + menu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + private boolean nodeClickAction(Object pressedNode) { + if (pressedNode == null) { + return false; + } + + if (pressedNode instanceof QuickTabsChildNode) { + QuickTabsChildNode childNode = (QuickTabsChildNode) pressedNode; + return mainWindow.getTabbedPane().showNode(childNode.getJNode()); + } + + return false; + } + + private void fillPinParentNode() { + mainWindow.getTabbedPane().getPinnedTabs().forEach((contentPanel) -> pinParentNode.addJNode(contentPanel.getNode())); + } + + @Override + public void valueChanged(TreeSelectionEvent event) { + DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) getLastSelectedPathComponent(); + if (selectedNode != null) { + 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 }); + } + } + + public void loadSettings() { + Font font = mainWindow.getSettings().getFont(); + Font largerFont = font.deriveFont(font.getSize() + 2.f); + + setFont(largerFont); + } + + public void dispose() { + mainWindow.getTabbedPane().removeTabStateListener(this); + } + + private class Root extends DefaultMutableTreeNode { + + } + + private class CellRenderer extends DefaultTreeCellRenderer { + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, + boolean hasFocus) { + Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + if (value instanceof QuickTabsBaseNode) { + QuickTabsBaseNode quickTabsNode = (QuickTabsBaseNode) value; + setIcon(quickTabsNode.getIcon()); + } + return c; + } + } +} 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 6947fbe09..13a796a63 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 @@ -38,6 +38,8 @@ public class TabComponent extends JPanel { private final ContentPanel contentPanel; private JLabel label; + private JButton pinBtn; + private JButton closeBtn; public TabComponent(TabbedPane tabbedPane, ContentPanel contentPanel) { this.tabbedPane = tabbedPane; @@ -74,7 +76,18 @@ public class TabComponent extends JPanel { ((JEditableNode) node).addChangeListener(c -> label.setText(buildTabTitle(node))); } - final JButton closeBtn = new JButton(); + pinBtn = new JButton(); + pinBtn.setIcon(Icons.PIN); + pinBtn.setRolloverIcon(Icons.PIN_HOVERED); + pinBtn.setRolloverEnabled(true); + pinBtn.setOpaque(false); + pinBtn.setUI(new BasicButtonUI()); + pinBtn.setContentAreaFilled(false); + pinBtn.setBorder(null); + pinBtn.setBorderPainted(false); + pinBtn.addActionListener(e -> togglePin()); + + closeBtn = new JButton(); closeBtn.setIcon(Icons.CLOSE_INACTIVE); closeBtn.setRolloverIcon(Icons.CLOSE); closeBtn.setRolloverEnabled(true); @@ -84,13 +97,13 @@ public class TabComponent extends JPanel { closeBtn.setFocusable(false); closeBtn.setBorder(null); closeBtn.setBorderPainted(false); - closeBtn.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel)); + closeBtn.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel, true)); MouseAdapter clickAdapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (SwingUtilities.isMiddleMouseButton(e)) { - tabbedPane.closeCodePanel(contentPanel); + tabbedPane.closeCodePanel(contentPanel, true); } else if (SwingUtilities.isRightMouseButton(e)) { JPopupMenu menu = createTabPopupMenu(contentPanel); menu.show(e.getComponent(), e.getX(), e.getY()); @@ -105,10 +118,38 @@ public class TabComponent extends JPanel { addListenerForDnd(); add(label); - add(closeBtn); + updateCloseOrPinButton(); setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); } + public void updateCloseOrPinButton() { + if (contentPanel.isPinned()) { + if (closeBtn.isShowing()) { + remove(closeBtn); + } + if (!pinBtn.isShowing()) { + add(pinBtn); + } + } + if (!contentPanel.isPinned()) { + if (pinBtn.isShowing()) { + remove(pinBtn); + } + if (!closeBtn.isShowing()) { + add(closeBtn); + } + } + } + + public void togglePin() { + contentPanel.setPinned(!contentPanel.isPinned()); + updateCloseOrPinButton(); + if (contentPanel.isPinned()) { + tabbedPane.advanceTab(this); + } + tabbedPane.notifyTabStateChange(this, true); + } + private void addListenerForDnd() { if (tabbedPane.getDnd() == null) { return; @@ -150,8 +191,22 @@ public class TabComponent extends JPanel { menu.addSeparator(); } + if (contentPanel.isPinnable()) { + String pinTitle = contentPanel.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()); + menu.add(unpinAll); + } + JMenuItem closeTab = new JMenuItem(NLS.str("tabs.close")); - closeTab.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel)); + closeTab.addActionListener(e -> tabbedPane.closeCodePanel(contentPanel, true)); + if (contentPanel.isPinned()) { + closeTab.setEnabled(false); + } menu.add(closeTab); List tabs = tabbedPane.getTabs(); @@ -160,14 +215,14 @@ public class TabComponent extends JPanel { closeOther.addActionListener(e -> { for (ContentPanel panel : tabs) { if (panel != contentPanel) { - tabbedPane.closeCodePanel(panel); + tabbedPane.closeCodePanel(panel, true); } } }); menu.add(closeOther); JMenuItem closeAll = new JMenuItem(NLS.str("tabs.closeAll")); - closeAll.addActionListener(e -> tabbedPane.closeAllTabs()); + closeAll.addActionListener(e -> tabbedPane.closeAllTabs(true)); menu.add(closeAll); if (contentPanel != ListUtils.last(tabs)) { @@ -180,7 +235,7 @@ public class TabComponent extends JPanel { pastCurrentPanel = true; } } else { - tabbedPane.closeCodePanel(panel); + tabbedPane.closeCodePanel(panel, true); } } }); @@ -212,4 +267,8 @@ public class TabComponent extends JPanel { } return node.getName(); } + + public ContentPanel getContentPanel() { + return contentPanel; + } } 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 f3a391b9e..b7cef842a 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 @@ -49,6 +49,8 @@ public class TabbedPane extends JTabbedPane { private final transient MainWindow mainWindow; private final transient Map tabsMap = new HashMap<>(); + + private final ArrayList tabStatesListeners = new ArrayList<>(); private final transient JumpManager jumps = new JumpManager(); private transient ContentPanel curTab; @@ -343,6 +345,7 @@ public class TabbedPane extends JTabbedPane { viewState = new EditorViewState(panel.getNode(), "", 0, EditorViewState.ZERO); } viewState.setActive(panel == selected); + viewState.setPinned(panel.isPinned()); states.add(viewState); } return states; @@ -356,6 +359,17 @@ public class TabbedPane extends JTabbedPane { 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() { @@ -386,11 +400,42 @@ public class TabbedPane extends JTabbedPane { } public void closeCodePanel(ContentPanel contentPanel) { + closeCodePanel(contentPanel, false); + } + + 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); + } + public List getTabs() { List list = new ArrayList<>(getTabCount()); for (int i = 0; i < getTabCount(); i++) { @@ -399,10 +444,41 @@ 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); } + public @Nullable TabComponent getTabComponentByNode(JNode node) { + Component component = getTabComponentAt(indexOfComponent(getTabByNode(node))); + if (!(component instanceof TabComponent)) { + return null; + } + + return (TabComponent) component; + } + private @Nullable ContentPanel getContentPanel(JNode node) { ContentPanel panel = getTabByNode(node); if (panel != null) { @@ -417,6 +493,10 @@ public class TabbedPane extends JTabbedPane { return newPanel; } + public void unpinAll() { + getPinnedTabComponents().forEach(TabComponent::togglePin); + } + public void refresh(JNode node) { ContentPanel panel = getTabByNode(node); if (panel != null) { @@ -464,8 +544,12 @@ public class TabbedPane extends JTabbedPane { } public void closeAllTabs() { + closeAllTabs(false); + } + + public void closeAllTabs(boolean considerPins) { for (ContentPanel panel : getTabs()) { - closeCodePanel(panel); + closeCodePanel(panel, considerPins); } } 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 0e4866f08..278a1a8fe 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 @@ -32,4 +32,9 @@ public class StartPageNode extends JNode { public JClass getJParent() { return null; } + + @Override + public boolean isPinnable() { + 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 5d741d1e6..86f40b1c2 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Icons.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Icons.java @@ -15,6 +15,11 @@ public class Icons { public static final ImageIcon SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall"); + public static final ImageIcon PIN = UiUtils.openSvgIcon("nodes/pin"); + 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 STATIC = openSvgIcon("nodes/staticMark"); public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark"); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 398c2211a..8d25b928d 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -4,6 +4,7 @@ import java.awt.Component; import java.awt.Image; import java.awt.MouseInfo; import java.awt.Point; +import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.datatransfer.Clipboard; @@ -12,6 +13,7 @@ import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -24,9 +26,12 @@ import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JOptionPane; +import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.TestOnly; @@ -296,6 +301,31 @@ public class UiUtils { return pos; } + public static TreeNode getTreeNodeUnderMouse(JTree tree, MouseEvent mouseEvent) { + TreePath path = tree.getClosestPathForLocation(mouseEvent.getX(), mouseEvent.getY()); + if (path == null) { + return null; + } + // allow 'closest' path only at the right of the item row + Rectangle pathBounds = tree.getPathBounds(path); + if (pathBounds != null) { + int y = mouseEvent.getY(); + if (y < pathBounds.y || y > (pathBounds.y + pathBounds.height)) { + return null; + } + if (mouseEvent.getX() < pathBounds.x) { + // exclude expand/collapse events + return null; + } + } + Object obj = path.getLastPathComponent(); + if (obj instanceof TreeNode) { + tree.setSelectionPath(path); + return (TreeNode) obj; + } + return null; + } + public static String getEnvVar(String varName, String defValue) { String envVal = System.getenv(varName); if (envVal == null) { 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 bb6a21ae2..5bcbd1e15 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -10,6 +10,7 @@ menu.flatten=Codepaket erweitern menu.heapUsageBar=Speicherverbrauchsleiste anzeigen menu.alwaysSelectOpened=Immer geöffnete Datei/Klasse auswählen #menu.dock_log=Dock log viewer +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=Navigation menu.text_search=Textsuche menu.class_search=Klassen-Suche @@ -58,6 +59,7 @@ file.exit=Beenden tree.sources_title=Quelltexte tree.resources_title=Ressourcen tree.loading=Laden… +#tree.pinned_tabs=Pinned Tabs progress.load=Laden progress.save_mappings=Zuordnungen exportieren @@ -79,6 +81,9 @@ search.find=Suchen tabs.copy_class_name=Klassennamen kopieren tabs.close=Schließen tabs.closeOthers=Andere schließen +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=Alles schließen tabs.closeAllRight=Schließe alles rechts tabs.code=Code 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 3bc33fc25..57ce2a500 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -10,6 +10,7 @@ menu.flatten=Show flatten packages menu.heapUsageBar=Show memory usage bar menu.alwaysSelectOpened=Always Select Opened File/Class menu.dock_log=Dock log viewer +menu.dock_quick_tabs=Dock quick tabs menu.navigation=Navigation menu.text_search=Text search menu.class_search=Class search @@ -58,6 +59,7 @@ tree.input_scripts=Scripts tree.sources_title=Source code tree.resources_title=Resources tree.loading=Loading... +tree.pinned_tabs=Pinned Tabs progress.load=Loading progress.save_mappings=Saving mappings @@ -79,6 +81,9 @@ search.results=%s%d results tabs.copy_class_name=Copy Name tabs.close=Close tabs.closeOthers=Close Others +tabs.unpin=Unpin +tabs.unpin_all=Unpin All +tabs.pin=Pin tabs.closeAll=Close All tabs.closeAllRight=Close All Right tabs.code=Code 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 1728233a4..296489637 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -10,6 +10,7 @@ menu.flatten=Mostrar paquetes en vista plana #menu.heapUsageBar= #menu.alwaysSelectOpened=Always Select Opened File/Class #menu.dock_log=Dock log viewer +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=Navegación menu.text_search=Buscar texto menu.class_search=Buscar clase @@ -58,6 +59,7 @@ file.exit=Salir tree.sources_title=Código fuente tree.resources_title=Recursos tree.loading=Cargando... +#tree.pinned_tabs=Pinned Tabs progress.load=Cargando #progress.save_mappings= @@ -79,6 +81,9 @@ search.find=Buscar tabs.copy_class_name=Copy Name tabs.close=Cerrar tabs.closeOthers=Cerrar otros +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=Cerrar todo tabs.closeAllRight=Cierra todo a la derecha #tabs.code= 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 8ab53d3a4..6a197b304 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -10,6 +10,7 @@ menu.flatten=Tampilkan paket yang diratakan menu.heapUsageBar=Tampilkan penggunaan memori menu.alwaysSelectOpened=Selalu Pilih Berkas/Kelas yang Terbuka menu.dock_log=Kaitkan pemantau log +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=Navigasi menu.text_search=Pencarian Teks menu.class_search=Pencarian Kelas @@ -58,6 +59,7 @@ tree.input_scripts=Skrip tree.sources_title=Kode Sumber tree.resources_title=Sumber Daya tree.loading=Memuat... +#tree.pinned_tabs=Pinned Tabs progress.load=Memuat progress.save_mappings=Menyimpan pemetaan @@ -79,6 +81,9 @@ search.results=%s%d hasil tabs.copy_class_name=Salin Nama tabs.close=Tutup tabs.closeOthers=Tutup yang Lain +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=Tutup Semua tabs.closeAllRight=Tutup Semua yang Kanan tabs.code=Kode 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 48cd11dbe..f3859a6ff 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -10,6 +10,7 @@ menu.flatten=플랫 패키지 표시 menu.heapUsageBar=메모리 사용량 표시 menu.alwaysSelectOpened=항상 열린 파일/클래스 선택 #menu.dock_log=Dock log viewer +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=네비게이션 menu.text_search=텍스트 검색 menu.class_search=클래스 검색 @@ -58,6 +59,7 @@ start_page.recent=최근 프로젝트 tree.sources_title=소스코드 tree.resources_title=리소스 tree.loading=로딩중... +#tree.pinned_tabs=Pinned Tabs progress.load=로딩중 progress.save_mappings=매핑 내보내는 중 @@ -79,6 +81,9 @@ search.find=찾기 tabs.copy_class_name=이름 복사 tabs.close=닫기 tabs.closeOthers=이 탭을 제외하고 닫기 +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=모두 닫기 tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오 tabs.code=코드 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 fbaad8b33..cc37ad523 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -10,6 +10,7 @@ menu.flatten=Mostrar pacotes achatados menu.heapUsageBar=Mostrar uso de memória menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta #menu.dock_log=Dock log viewer +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=Navegação menu.text_search=Buscar por texto menu.class_search=Buscar por classe @@ -58,6 +59,7 @@ start_page.recent=Projetos recentes tree.sources_title=Código fonte tree.resources_title=Recursos tree.loading=Carregando... +#tree.pinned_tabs=Pinned Tabs progress.load=Carregando #progress.save_mappings=Saving mappings @@ -79,6 +81,9 @@ search.find=Encontrar tabs.copy_class_name=Copiar nome tabs.close=Fechar tabs.closeOthers=Fechar outros +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=Fechar todos tabs.closeAllRight=Feche tudo à direita tabs.code=Código 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 c7d63e58a..cd81390ef 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -10,6 +10,7 @@ menu.flatten=Плоская структура пакетов menu.heapUsageBar=Использование ОЗУ menu.alwaysSelectOpened=Выбирать открытый файл/класс menu.dock_log=Просмотр логов в панели +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=Навигация menu.text_search=Поиск строк menu.class_search=Поиск классов @@ -58,6 +59,7 @@ tree.input_scripts=Скрипты tree.sources_title=Исходный код tree.resources_title=Ресурсы tree.loading=Загрузка... +#tree.pinned_tabs=Pinned Tabs progress.load=Загрузка progress.save_mappings=Сохранить маппинги @@ -79,6 +81,9 @@ search.results=%s%d результатов tabs.copy_class_name=Копировать имя tabs.close=Закрыть tabs.closeOthers=Закрыть другие +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=Закрыть все tabs.closeAllRight=Закройте все справа tabs.code=Код 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 f2a3dc3fc..25f6df82c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -10,6 +10,7 @@ menu.flatten=展开显示代码包 menu.heapUsageBar=显示内存使用栏 menu.alwaysSelectOpened=始终选中打开的文件/类 menu.dock_log=停靠日志查看器 +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=导航 menu.text_search=文本搜索 menu.class_search=类名搜索 @@ -58,6 +59,7 @@ tree.input_scripts=脚本 tree.sources_title=源代码 tree.resources_title=资源文件 tree.loading=加载中… +#tree.pinned_tabs=Pinned Tabs progress.load=正在加载 progress.save_mappings=导出映射 @@ -79,6 +81,9 @@ search.results=%s%d 个结果 tabs.copy_class_name=复制名称 tabs.close=关闭 tabs.closeOthers=关闭其他 +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=关闭全部 tabs.closeAllRight=关闭右边的所有 tabs.code=代码 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 181778991..912c3618c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -10,6 +10,7 @@ menu.flatten=展開顯示套件 menu.heapUsageBar=顯示記憶體使用率條 menu.alwaysSelectOpened=總是選擇已開啟的檔案/類別 menu.dock_log=固定記錄檔檢視器 +#menu.dock_quick_tabs=Dock quick tabs menu.navigation=瀏覽 menu.text_search=文字搜尋 menu.class_search=類別搜尋 @@ -58,6 +59,7 @@ tree.input_scripts=腳本 tree.sources_title=原始碼 tree.resources_title=資源 tree.loading=載入中... +#tree.pinned_tabs=Pinned Tabs progress.load=載入中 progress.save_mappings=正在匯出對應 @@ -79,6 +81,9 @@ search.results=%s%d 個結果 tabs.copy_class_name=複製名稱 tabs.close=關閉 tabs.closeOthers=關閉其他 +#tabs.unpin=Unpin +#tabs.unpin_all=Unpin All +#tabs.pin=Pin tabs.closeAll=關閉全部 tabs.closeAllRight=關閉右邊的所有 tabs.code=程式碼 diff --git a/jadx-gui/src/main/resources/icons/nodes/pin.svg b/jadx-gui/src/main/resources/icons/nodes/pin.svg new file mode 100644 index 000000000..9199fc045 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/pin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/pinHovered.svg b/jadx-gui/src/main/resources/icons/nodes/pinHovered.svg new file mode 100644 index 000000000..9786d2a4e --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/pinHovered.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/pinHovered_dark.svg b/jadx-gui/src/main/resources/icons/nodes/pinHovered_dark.svg new file mode 100644 index 000000000..bed026f79 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/pinHovered_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/jadx-gui/src/main/resources/icons/nodes/pin_dark.svg b/jadx-gui/src/main/resources/icons/nodes/pin_dark.svg new file mode 100644 index 000000000..f287b61b7 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/nodes/pin_dark.svg @@ -0,0 +1,5 @@ + + + + +