From 6bf358fc6694c7cd3596c28af0541542a63780db Mon Sep 17 00:00:00 2001 From: LBJ-the-GOAT <66319139+LBJ-the-GOAT@users.noreply.github.com> Date: Fri, 16 Apr 2021 18:37:11 +0800 Subject: [PATCH] feat(gui): improve exclude package feature (#1151) (PR #1152) * include & exclude multiple packages at the same time * use to tree instead of list to display packages. Co-authored-by: tobias --- .../src/main/java/jadx/gui/JadxWrapper.java | 5 + .../java/jadx/gui/ui/ExcludePkgDialog.java | 298 ++++++++++++++++++ .../java/jadx/gui/ui/JPackagePopupMenu.java | 13 + .../resources/i18n/Messages_de_DE.properties | 7 + .../resources/i18n/Messages_en_US.properties | 7 + .../resources/i18n/Messages_es_ES.properties | 7 + .../resources/i18n/Messages_ko_KR.properties | 7 + .../resources/i18n/Messages_zh_CN.properties | 7 + 8 files changed, 351 insertions(+) create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/ExcludePkgDialog.java diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 654bafc09..66216339f 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -112,6 +112,11 @@ public class JadxWrapper { return Arrays.asList(excludedPackages.split("[ ]+")); } + public void setExcludedPackages(List packagesToExclude) { + settings.setExcludedPackages(String.join(" ", packagesToExclude).trim()); + settings.sync(); + } + public void addExcludedPackage(String packageToExclude) { String newExclusion = settings.getExcludedPackages() + ' ' + packageToExclude; settings.setExcludedPackages(newExclusion.trim()); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ExcludePkgDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/ExcludePkgDialog.java new file mode 100644 index 000000000..7f04b759f --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/ExcludePkgDialog.java @@ -0,0 +1,298 @@ +package jadx.gui.ui; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.*; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.swing.*; +import javax.swing.tree.*; + +import jadx.api.JavaPackage; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; + +public class ExcludePkgDialog extends JDialog { + private static final long serialVersionUID = -1111111202104151030L; + private static final ImageIcon PACKAGE_ICON = UiUtils.openIcon("package_obj"); + + private final transient MainWindow mainWindow; + private transient JTree tree; + private transient DefaultMutableTreeNode treeRoot; + private final transient List roots = new ArrayList<>(); + + public ExcludePkgDialog(MainWindow mainWindow) { + super(mainWindow); + this.mainWindow = mainWindow; + initUI(); + UiUtils.addEscapeShortCutToDispose(this); + initPackageList(); + } + + private void initUI() { + setTitle(NLS.str("exclude_dialog.title")); + tree = new JTree(); + tree.setRowHeight(-1); + treeRoot = new DefaultMutableTreeNode("Packages"); + DefaultTreeModel treeModel = new DefaultTreeModel(treeRoot); + tree.setModel(treeModel); + tree.setCellRenderer(new PkgListCellRenderer()); + JScrollPane listPanel = new JScrollPane(tree); + listPanel.setBorder(BorderFactory.createLineBorder(Color.black)); + tree.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + PkgNode node = (PkgNode) path.getLastPathComponent(); + node.toggle(); + repaint(); + } + } + }); + + JPanel actionPanel = new JPanel(); + BoxLayout boxLayout = new BoxLayout(actionPanel, BoxLayout.LINE_AXIS); + actionPanel.setLayout(boxLayout); + actionPanel.add(new Label(" ")); + JButton btnOk = new JButton(NLS.str("exclude_dialog.ok")); + JButton btnAll = new JButton(NLS.str("exclude_dialog.select_all")); + JButton btnInvert = new JButton(NLS.str("exclude_dialog.invert")); + JButton btnDeselect = new JButton(NLS.str("exclude_dialog.deselect")); + actionPanel.add(btnDeselect); + actionPanel.add(btnInvert); + actionPanel.add(btnAll); + actionPanel.add(new Label(" ")); + actionPanel.add(btnOk); + + JPanel mainPane = new JPanel(new BorderLayout(5, 5)); + mainPane.add(listPanel, BorderLayout.CENTER); + mainPane.add(actionPanel, BorderLayout.SOUTH); + mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + getContentPane().add(mainPane); + pack(); + setSize(600, 700); + setLocationRelativeTo(null); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setModalityType(ModalityType.MODELESS); + + btnOk.addActionListener(e -> { + mainWindow.getWrapper().setExcludedPackages(getExcludes()); + mainWindow.reOpenFile(); + dispose(); + }); + btnAll.addActionListener(e -> { + roots.forEach(p -> p.setSelected(true)); + tree.updateUI(); + }); + btnDeselect.addActionListener(e -> { + roots.forEach(p -> p.setSelected(false)); + tree.updateUI(); + }); + btnInvert.addActionListener(e -> { + roots.forEach(PkgNode::toggle); + tree.updateUI(); + }); + } + + private void initPackageList() { + List pkgs = mainWindow.getWrapper().getDecompiler().getPackages() + .stream() + .map(JavaPackage::getFullName) + .collect(Collectors.toList()); + getPackageTree(pkgs).forEach(treeRoot::add); + initCheckbox(); + tree.expandPath(new TreePath(treeRoot.getPath())); + } + + private List getPackageTree(List names) { + List roots = new ArrayList<>(); + Set nameSet = new HashSet<>(); + Map> childMap = new HashMap<>(); + for (String name : names) { + String parent = ""; + int last = 0; + do { + int pos = name.indexOf(".", last); + if (pos == -1) { + pos = name.length(); + } + String fullName = name.substring(0, pos); + if (!nameSet.contains(fullName)) { + nameSet.add(fullName); + PkgNode node = new PkgNode(fullName, name.substring(last, pos)); + if (!parent.isEmpty()) { + childMap.computeIfAbsent(parent, k -> new ArrayList<>()) + .add(node); + } else { + roots.add(node); + } + } + parent = fullName; + last = pos + 1; + } while (last < name.length()); + } + addToParent(null, roots, childMap); + return this.roots; + } + + private PkgNode addToParent(PkgNode parent, List roots, Map> childMap) { + for (PkgNode root : roots) { + String tempFullName = root.getFullName(); + do { + List children = childMap.get(tempFullName); + if (children != null) { + if (children.size() == 1) { + PkgNode next = children.get(0); + next.name = root.name + "." + next.name; + tempFullName = next.fullName; + next.fullName = root.fullName; + root = next; + continue; + } else { + addToParent(root, children, childMap); + } + } + if (parent == null) { + this.roots.add(root); + } else { + parent.add(root); + } + break; + } while (true); + } + return parent; + } + + private List getExcludes() { + List excludes = new ArrayList<>(); + walkTree(true, p -> excludes.add(p.getFullName())); + return excludes; + } + + private void initCheckbox() { + Font tmp = mainWindow.getSettings().getFont(); + Font font = tmp.deriveFont(tmp.getSize() + 1.f); + Set excluded = new HashSet<>(mainWindow.getWrapper().getExcludedPackages()); + walkTree(false, p -> p.initCheckbox(excluded.contains(p.getFullName()), font)); + } + + private void walkTree(boolean findSelected, Consumer consumer) { + List queue = new ArrayList<>(roots); + for (int i = 0; i < queue.size(); i++) { + PkgNode node = queue.get(i); + if (findSelected && node.isSelected()) { + consumer.accept(node); + } else { + if (!findSelected) { + consumer.accept(node); + } + for (int j = 0; j < node.getChildCount(); j++) { + queue.add((PkgNode) node.getChildAt(j)); + } + } + } + } + + private static class PkgNode extends DefaultMutableTreeNode { + private static final long serialVersionUID = -1111111202104151430L; + + String name; + String fullName; + JCheckBox checkbox; + + PkgNode(String fullName, String name) { + this.name = name; + this.fullName = fullName; + } + + void initCheckbox(boolean select, Font font) { + if (!select) { + if (getParent() instanceof PkgNode) { + select = ((PkgNode) getParent()).isSelected(); + } + } + checkbox = new JCheckBox(name, select); + checkbox.setFont(font); + } + + boolean toggle() { + boolean selected = !checkbox.isSelected(); + setSelected(selected); + toggleParents(selected); + return selected; + } + + void toggleParents(boolean select) { + if (getParent() instanceof PkgNode) { + PkgNode p = ((PkgNode) getParent()); + if (select) { + select = p.isChildrenAllSelected(); + if (select) { + p.checkbox.setSelected(true); + p.toggleParents(true); + } + } else { + p.checkbox.setSelected(false); + p.toggleParents(false); + } + } + } + + void setSelected(boolean select) { + checkbox.setSelected(select); + for (int i = 0; i < getChildCount(); i++) { + ((PkgNode) getChildAt(i)).setSelected(select); + } + } + + boolean isSelected() { + return checkbox.isSelected(); + } + + String getFullName() { + return fullName; + } + + String getDisplayName() { + return name; + } + + boolean isChildrenAllSelected() { + for (int i = 0; i < getChildCount(); i++) { + if (!((PkgNode) getChildAt(i)).isSelected()) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return name; + } + } + + private static class PkgListCellRenderer extends DefaultTreeCellRenderer { + private static final long serialVersionUID = -1111111202104151235L; + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, + boolean hasFocus) { + if (value instanceof PkgNode) { + PkgNode node = (PkgNode) value; + node.checkbox.setBackground(Color.white); + node.checkbox.setForeground(Color.black); + return node.checkbox; + } + Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + setIcon(PACKAGE_ICON); + return c; + } + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/JPackagePopupMenu.java b/jadx-gui/src/main/java/jadx/gui/ui/JPackagePopupMenu.java index 77ab7f0d8..e9ec4a9c6 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/JPackagePopupMenu.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/JPackagePopupMenu.java @@ -1,5 +1,6 @@ package jadx.gui.ui; +import java.awt.event.ActionEvent; import java.util.Arrays; import java.util.List; @@ -25,6 +26,7 @@ class JPackagePopupMenu extends JPopupMenu { this.mainWindow = mainWindow; add(makeExcludeItem(pkg)); + add(makeExcludeItem()); JMenuItem menuItem = makeRenameMenuItem(pkg); if (menuItem != null) { add(menuItem); @@ -115,4 +117,15 @@ class JPackagePopupMenu extends JPopupMenu { }); return excludeItem; } + + private JMenuItem makeExcludeItem() { + return new JMenuItem(new AbstractAction(NLS.str("popup.exclude_packages")) { + private static final long serialVersionUID = -1111111202104151028L; + + @Override + public void actionPerformed(ActionEvent e) { + new ExcludePkgDialog(mainWindow).setVisible(true); + } + }); + } } 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 e2bd282a4..fdf7d62ff 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -183,12 +183,19 @@ popup.select_all=Alle auswählen popup.find_usage=Verwendung suchen popup.go_to_declaration=Zur Erklärung gehen popup.exclude=Ausschließen +#popup.exclude_packages=Exclude packages #popup.add_comment=Comment #popup.search_comment=Search comments popup.rename=Umbennen #popup.search= #popup.search_global= +#exclude_dialog.title=Package Selector +#exclude_dialog.ok=OK +#exclude_dialog.select_all=Select all +#exclude_dialog.deselect=Deselect +#exclude_dialog.invert=Invert + confirm.save_as_title=Speichern unter bestätigen confirm.save_as_message=%s existiert bereits.\nErsetzen? confirm.not_saved_title=Projekt speichern 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 1e5071e05..d329df65b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -183,12 +183,19 @@ popup.select_all=Select All popup.find_usage=Find Usage popup.go_to_declaration=Go to declaration popup.exclude=Exclude +popup.exclude_packages=Exclude packages popup.add_comment=Comment popup.search_comment=Search comments popup.rename=Rename popup.search=Search "%s" popup.search_global=Global Search "%s" +exclude_dialog.title=Package Selector +exclude_dialog.ok=OK +exclude_dialog.select_all=Select all +exclude_dialog.deselect=Deselect +exclude_dialog.invert=Invert + confirm.save_as_title=Confirm Save as confirm.save_as_message=%s already exists.\nDo you want to replace it? confirm.not_saved_title=Save project 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 9ba2120e1..8e6492652 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -183,12 +183,19 @@ popup.select_all=Seleccionar todo #popup.find_usage= #popup.go_to_declaration= #popup.exclude= +#popup.exclude_packages=Exclude packages #popup.add_comment=Comment #popup.search_comment=Search comments popup.rename=Nimeta ümber #popup.search= #popup.search_global= +#exclude_dialog.title=Package Selector +#exclude_dialog.ok=OK +#exclude_dialog.select_all=Select all +#exclude_dialog.deselect=Deselect +#exclude_dialog.invert=Invert + #confirm.save_as_title= #confirm.save_as_message= #confirm.not_saved_title= 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 da15b78d1..c96eaa589 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -183,12 +183,19 @@ popup.select_all=모두 선택 popup.find_usage=사용 찾기 popup.go_to_declaration=선언문으로 이동 popup.exclude=제외 +#popup.exclude_packages=Exclude packages popup.add_comment=주석 popup.search_comment=주석 검색 popup.rename=이름 바꾸기 popup.search="%s" 검색 popup.search_global="%s" 전역 검색 +#exclude_dialog.title=Package Selector +#exclude_dialog.ok=OK +#exclude_dialog.select_all=Select all +#exclude_dialog.deselect=Deselect +#exclude_dialog.invert=Invert + confirm.save_as_title=다른 이름으로 저장 확인 confirm.save_as_message=%s이(가) 이미 있습니다.\n바꾸시겠습니까? confirm.not_saved_title=프로젝트 저장 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 4b001ceb1..9a22b240b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -183,12 +183,19 @@ popup.select_all=全选 popup.find_usage=查找用例 popup.go_to_declaration=跳到声明 popup.exclude=排除 +#popup.exclude_packages=Exclude packages #popup.add_comment=Comment #popup.search_comment=Search comments popup.rename=改名 #popup.search= #popup.search_global= +#exclude_dialog.title=Package Selector +#exclude_dialog.ok=OK +#exclude_dialog.select_all=Select all +#exclude_dialog.deselect=Deselect +#exclude_dialog.invert=Invert + confirm.save_as_title=确认另存为 confirm.save_as_message=%s 已存在。\n你想替换它吗? confirm.not_saved_title=保存项目