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 <tobias.hotmail.com>
This commit is contained in:
LBJ-the-GOAT
2021-04-16 18:37:11 +08:00
committed by GitHub
parent e8f57d3ace
commit 6bf358fc66
8 changed files with 351 additions and 0 deletions
@@ -112,6 +112,11 @@ public class JadxWrapper {
return Arrays.asList(excludedPackages.split("[ ]+"));
}
public void setExcludedPackages(List<String> packagesToExclude) {
settings.setExcludedPackages(String.join(" ", packagesToExclude).trim());
settings.sync();
}
public void addExcludedPackage(String packageToExclude) {
String newExclusion = settings.getExcludedPackages() + ' ' + packageToExclude;
settings.setExcludedPackages(newExclusion.trim());
@@ -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<PkgNode> 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<String> 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<PkgNode> getPackageTree(List<String> names) {
List<PkgNode> roots = new ArrayList<>();
Set<String> nameSet = new HashSet<>();
Map<String, List<PkgNode>> 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<PkgNode> roots, Map<String, List<PkgNode>> childMap) {
for (PkgNode root : roots) {
String tempFullName = root.getFullName();
do {
List<PkgNode> 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<String> getExcludes() {
List<String> 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<String> excluded = new HashSet<>(mainWindow.getWrapper().getExcludedPackages());
walkTree(false, p -> p.initCheckbox(excluded.contains(p.getFullName()), font));
}
private void walkTree(boolean findSelected, Consumer<PkgNode> consumer) {
List<PkgNode> 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;
}
}
}
@@ -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);
}
});
}
}
@@ -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
@@ -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
@@ -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=
@@ -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=프로젝트 저장
@@ -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=保存项目