diff --git a/jadx-gui/src/main/java/jadx/gui/ui/FilterableTreeModel.java b/jadx-gui/src/main/java/jadx/gui/ui/FilterableTreeModel.java
new file mode 100644
index 00000000000..81f7ed53adb
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/ui/FilterableTreeModel.java
@@ -0,0 +1,282 @@
+package jadx.gui.ui;
+
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.swing.JTree;
+import javax.swing.SwingUtilities;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jadx.api.ResourceType;
+import jadx.gui.jobs.SimpleTask;
+import jadx.gui.jobs.TaskStatus;
+import jadx.gui.treemodel.JClass;
+import jadx.gui.treemodel.JMethod;
+import jadx.gui.treemodel.JNode;
+import jadx.gui.treemodel.JResource;
+import jadx.gui.treemodel.JRoot;
+import jadx.gui.treemodel.TextNode;
+import jadx.gui.utils.UiUtils;
+
+/**
+ * A FilterableTreeModel provides dynamic filtering over the decompilation package tree.
+ * This filtering is done at the model level, to prevent oddities such as zero-width rows disrupting
+ * a keyboard user experience or otherwise causing problems.
+ *
+ * This class is specialized to filter the main UI pane displaying the decompilation tree. If the
+ * fully qualified path of a tree element is obtainable, filtering will check against that, falling
+ * back to the standard name if the former is unavailable.
+ */
+class FilterableTreeModel extends DefaultTreeModel {
+ private static final Logger LOG = LoggerFactory.getLogger(FilterableTreeModel.class);
+
+ private final MainWindow mainWindow;
+
+ /**
+ * The filter string
+ */
+ private volatile String filter;
+
+ /**
+ * All nodes to filtered paths (including middle nodes)
+ */
+ private final Set filteredTreeNodes;
+
+ /**
+ * Store nodes expanded automatically to prevent re-expand for nodes collapsed by user
+ */
+ private final Set autoExpandedNodes;
+
+ /**
+ * Very often 'getChild' method called for all children after 'getChildCount' call.
+ * Save children list for fast child return.
+ */
+ private final CacheNode cacheNode = new CacheNode();
+
+ public FilterableTreeModel(MainWindow mainWindow, TreeNode root) {
+ super(root);
+ this.mainWindow = mainWindow;
+ this.filter = "";
+ this.filteredTreeNodes = new HashSet<>();
+ this.autoExpandedNodes = new HashSet<>();
+ }
+
+ /**
+ * Sets the filter of the tree by pre-computing tree paths matching the filter and refreshing the
+ * tree's node structure.
+ *
+ * This calls `nodeStructureChanged` on the root of the tree, implying a complete refresh of the
+ * tree data. Unfortunately the declarative nature of the filtering makes it infeasible to give a
+ * more specific refresh, which would potentially preserve open more of the current state of the
+ * tree.
+ *
+ * @param newFilter the new filter string, or "" to unset the filter.
+ */
+ public synchronized void setFilter(String newFilter) {
+ this.filter = newFilter;
+ this.autoExpandedNodes.clear();
+ this.cacheNode.clear();
+ LOG.debug("New tree filter '{}'", newFilter);
+ applyFilterFieldOutline("");
+ collectFilteredPaths();
+ SwingUtilities.invokeLater(() -> this.nodeStructureChanged((TreeNode) getRoot()));
+ SwingUtilities.invokeLater(() -> expandVisibleFilteredNodes(mainWindow.getTree()));
+ }
+
+ /**
+ * Filter thread safe nodeStructureChanged event.
+ * This should ensure that all treeListeners get the same filter value per event.
+ */
+ @Override
+ public void nodeStructureChanged(TreeNode node) {
+ super.nodeStructureChanged(node);
+ }
+
+ /**
+ * If filter enabled, expand all visible nodes.
+ */
+ public void expandVisibleFilteredNodes(JTree tree) {
+ if (filter.isEmpty()) {
+ return;
+ }
+ Rectangle rect = tree.getVisibleRect();
+ int startRow = tree.getClosestRowForLocation(0, rect.y);
+ int bottom = rect.y + rect.height - 1;
+ while (true) {
+ int lastRow = tree.getClosestRowForLocation(0, bottom);
+ if (lastRow <= startRow) {
+ break;
+ }
+ // limit updates for one iteration
+ int last = Math.min(startRow + 20, lastRow);
+ for (int i = startRow; i <= lastRow; i++) {
+ TreePath path = tree.getPathForRow(i);
+ JNode node = (JNode) path.getLastPathComponent();
+ if (node instanceof JClass) {
+ // don't auto expand methods
+ } else {
+ if (autoExpandedNodes.add(node)) {
+ tree.expandPath(path);
+ }
+ }
+ }
+ startRow = last;
+ }
+ }
+
+ private void applyFilterFieldOutline(String outlineType) {
+ UiUtils.uiRun(() -> mainWindow.getTreeFilterField().putClientProperty("JComponent.outline", outlineType));
+ }
+
+ private void collectFilteredPaths() {
+ UiUtils.notUiThreadGuard();
+ filteredTreeNodes.clear();
+ if (filter.isEmpty()) {
+ return;
+ }
+
+ Object rootNode = this.getRoot();
+ if (!(rootNode instanceof JRoot)) {
+ return; // root node is null or of a different type
+ }
+ int nodesCount = 0;
+ int filteredCount = 0;
+ Enumeration en = ((JRoot) rootNode).depthFirstEnumeration();
+ while (en.hasMoreElements()) {
+ TreeNode node = en.nextElement();
+ nodesCount++;
+ if (matchesFilter(node)) {
+ addPathNodes(node);
+ filteredCount++;
+ }
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Total nodes: {}, filtered: {}", nodesCount, filteredCount);
+ }
+ if (filteredTreeNodes.isEmpty()) {
+ applyFilterFieldOutline("error");
+ }
+ }
+
+ private void addPathNodes(TreeNode node) {
+ if (!filteredTreeNodes.add(node)) {
+ return;
+ }
+ TreeNode parent = node.getParent();
+ while (parent != null) {
+ if (!filteredTreeNodes.add(parent)) {
+ break;
+ }
+ parent = parent.getParent();
+ }
+ }
+
+ /**
+ * Determines if a given node matches the current filter.
+ *
+ * @param node the node in question
+ * @return true if the filter is considered matched and the node should be displayed in the tree.
+ */
+ private boolean matchesFilter(Object node) {
+ if (node instanceof TextNode || node instanceof JMethod) {
+ return false;
+ }
+ if (node instanceof JResource) {
+ JResource res = (JResource) node;
+ if (res.getType() == JResource.JResType.FILE && res.getResFile().getType() == ResourceType.ARSC) {
+ loadInnerResources(res);
+ }
+ }
+ if (node instanceof JNode) {
+ JNode jNode = (JNode) node;
+ String name = jNode.makeString();
+ if (name == null) {
+ LOG.warn("Node {} has null UI string", node);
+ return false;
+ }
+ return name.toLowerCase().contains(filter.toLowerCase());
+ }
+ return false;
+ }
+
+ private void loadInnerResources(JResource res) {
+ // load inner resource of resource.arsc
+ SimpleTask loadTask = res.getLoadTask();
+ if (loadTask != null) {
+ try {
+ Future load = mainWindow.getBackgroundExecutor().executeWithFuture(loadTask);
+ load.get(); // wait for completion
+ } catch (Exception e) {
+ LOG.warn("Failed to load resource", e);
+ }
+ }
+ }
+
+ private static final class CacheNode {
+ private @Nullable TreeNode parent;
+ private final List children = new ArrayList<>();
+
+ public void clear() {
+ parent = null;
+ children.clear();
+ }
+ }
+
+ @Override
+ public Object getChild(Object parent, int index) {
+ if (cacheNode.parent == parent) {
+ return cacheNode.children.get(index);
+ }
+ if (filter.isEmpty() || parent instanceof JClass /* allow to expand and view all methods */) {
+ return super.getChild(parent, index);
+ }
+ int i = 0;
+ Enumeration extends TreeNode> en = ((TreeNode) parent).children();
+ while (en.hasMoreElements()) {
+ TreeNode child = en.nextElement();
+ if (filteredTreeNodes.contains(child)) {
+ if (i == index) {
+ return child;
+ }
+ i++;
+ }
+ }
+ throw new IllegalArgumentException("No child at index " + index);
+ }
+
+ @Override
+ public int getChildCount(Object parent) {
+ if (filter.isEmpty()) {
+ return super.getChildCount(parent);
+ }
+ TreeNode parentNode = (TreeNode) parent;
+ if (!filteredTreeNodes.contains(parentNode)) {
+ return 0;
+ }
+ cacheNode.parent = parentNode;
+ List children = cacheNode.children;
+ children.clear();
+
+ int count = 0;
+ Enumeration extends TreeNode> en = parentNode.children();
+ while (en.hasMoreElements()) {
+ TreeNode child = en.nextElement();
+ if (filteredTreeNodes.contains(child)) {
+ children.add(child);
+ count++;
+ }
+ }
+ return count;
+ }
+}
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 e609d4089ad..de69f94a8a1 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
@@ -32,13 +32,16 @@
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.Action;
+import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
@@ -48,9 +51,11 @@
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
+import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.JTree;
+import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
@@ -59,7 +64,6 @@
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
-import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
@@ -70,12 +74,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.extras.FlatInspector;
import com.formdev.flatlaf.extras.FlatUIDefaultsInspector;
import com.formdev.flatlaf.util.UIScale;
import ch.qos.logback.classic.Level;
+import hu.akarnokd.rxjava3.swing.SwingSchedulers;
+import io.reactivex.rxjava3.disposables.Disposable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
import jadx.api.JadxArgs;
import jadx.api.JavaClass;
@@ -172,9 +180,11 @@
import jadx.gui.utils.LafManager;
import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
+import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.dbg.UIWatchDog;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
+import jadx.gui.utils.rx.RxUtils;
import jadx.gui.utils.shortcut.ShortcutsController;
import jadx.gui.utils.ui.ActionHandler;
import jadx.gui.utils.ui.FileOpenerHelper;
@@ -214,7 +224,7 @@ public class MainWindow extends JFrame {
private transient JSplitPane quickTabsAndCodeSplitPane;
private JTree tree;
- private DefaultTreeModel treeModel;
+ private FilterableTreeModel treeModel;
private JRoot treeRoot;
private TabbedPane tabbedPane;
private HeapUsageBar heapUsageBar;
@@ -251,6 +261,10 @@ public class MainWindow extends JFrame {
private final transient RenameMappingsGui renameMappings;
+ private JTextField treeFilterField;
+ private Disposable treeFilterDisposable;
+ private Disposable treeScrollDisposable;
+
public MainWindow(JadxSettings settings) {
this.settings = settings;
this.project = new JadxProject(this);
@@ -813,6 +827,7 @@ public void initTree() {
treeRoot = new JRoot(this);
treeRoot.setFlatPackages(isFlattenPackage);
treeModel.setRoot(treeRoot);
+ treeFilterField.setText("");
addTreeCustomNodes();
treeRoot.update();
reloadTree();
@@ -1370,9 +1385,12 @@ private void initUI() {
mainPanel.add(treeSplitPane);
DefaultMutableTreeNode treeRootNode = new DefaultMutableTreeNode(NLS.str("msg.open_file"));
- treeModel = new DefaultTreeModel(treeRootNode);
+
+ treeModel = new FilterableTreeModel(this, treeRootNode);
tree = new JTree(treeModel);
+ tree.setLargeModel(true);
ToolTipManager.sharedInstance().registerComponent(tree);
+
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setFocusable(false);
tree.addFocusListener(new FocusAdapter() {
@@ -1454,16 +1472,38 @@ public void treeWillCollapse(TreeExpansionEvent event) {
progressPane = new ProgressPanel(this, true);
issuesPanel = new IssuesPanel(this);
+ treeFilterField = new JTextField();
+ TextStandardActions.attach(treeFilterField);
+ treeFilterField.setToolTipText(NLS.str("tree.filter"));
+ treeFilterField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
+ treeFilterField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, NLS.str("tree.filter"));
+ treeFilterField.registerKeyboardAction(ev -> treeFilterField.setText(""),
+ KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED);
+
+ treeFilterDisposable = RxUtils.textFieldChanges(treeFilterField)
+ .debounce(300, TimeUnit.MILLISECONDS)
+ .observeOn(Schedulers.newThread())
+ .subscribe(t -> treeModel.setFilter(treeFilterField.getText()));
+
+ JPanel filterPanel = new JPanel(new BorderLayout());
+ filterPanel.setBorder(BorderFactory.createEmptyBorder(5, 2, 2, 2));
+ filterPanel.add(treeFilterField, BorderLayout.CENTER);
+
JPanel leftPane = new JPanel(new BorderLayout());
JScrollPane treeScrollPane = new JScrollPane(tree);
treeScrollPane.setMinimumSize(new Dimension(100, 150));
+ treeScrollDisposable = RxUtils.scrollBarEvents(treeScrollPane.getVerticalScrollBar())
+ .observeOn(SwingSchedulers.edt())
+ .subscribe(v -> treeModel.expandVisibleFilteredNodes(tree));
JPanel bottomPane = new JPanel(new BorderLayout());
bottomPane.add(issuesPanel, BorderLayout.PAGE_START);
bottomPane.add(progressPane, BorderLayout.PAGE_END);
+ leftPane.add(filterPanel, BorderLayout.PAGE_START);
leftPane.add(treeScrollPane, BorderLayout.CENTER);
leftPane.add(bottomPane, BorderLayout.PAGE_END);
+
treeSplitPane.setLeftComponent(leftPane);
tabbedPane = new TabbedPane(this, tabsController);
@@ -1603,9 +1643,14 @@ private void closeWindow() {
closeAll();
UiUtils.uiRunAndWait(() -> {
- heapUsageBar.reset();
- editorThemeManager.unload();
- dispose();
+ try {
+ heapUsageBar.reset();
+ editorThemeManager.unload();
+ treeFilterDisposable.dispose();
+ treeScrollDisposable.dispose();
+ } finally {
+ dispose();
+ }
});
} catch (Exception e) {
LOG.error("Close window error", e);
@@ -1700,10 +1745,18 @@ public BackgroundExecutor getBackgroundExecutor() {
return backgroundExecutor;
}
+ public JTree getTree() {
+ return tree;
+ }
+
public JRoot getTreeRoot() {
return treeRoot;
}
+ public JTextField getTreeFilterField() {
+ return treeFilterField;
+ }
+
public JDebuggerPanel getDebuggerPanel() {
initDebuggerPanel();
return debuggerPanel;
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/rx/RxUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/rx/RxUtils.java
index f602350898f..67f7f03e9c0 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/rx/RxUtils.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/rx/RxUtils.java
@@ -1,10 +1,12 @@
package jadx.gui.utils.rx;
+import java.awt.event.AdjustmentListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.function.Supplier;
+import javax.swing.JScrollBar;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.event.ChangeListener;
@@ -57,6 +59,16 @@ public static Flowable spinnerEnterPress(final JSpinner spinner) {
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
}
+ public static Flowable scrollBarEvents(JScrollBar scrollBar) {
+ FlowableOnSubscribe source = emitter -> {
+ AdjustmentListener listener = e -> emitter.onNext(e.getValue());
+ scrollBar.addAdjustmentListener(listener);
+ emitter.setDisposable(new CustomDisposable(() -> scrollBar.removeAdjustmentListener(listener)));
+ };
+ return Flowable.create(source, BackpressureStrategy.LATEST)
+ .distinctUntilChanged(Integer::equals);
+ }
+
private static @NotNull KeyListener enterKeyListener(FlowableEmitter emitter, Supplier supplier) {
return new KeyAdapter() {
@Override
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 3de943da065..8befb72d06f 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties
@@ -68,6 +68,7 @@ tree.loading=Laden…
tree.pinned_tabs=Angepinnte Registerkarten
tree.open_tabs=Offene Registerkarten
tree.bookmarked_tabs=Mit Lesezeichen versehene Registerkarten
+#tree.filter=Tree filter
progress.load=Laden
progress.save_mappings=Zuordnungen werden gespeichert
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 12384ae8d38..1e97f0763d5 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
@@ -68,6 +68,7 @@ tree.loading=Loading...
tree.pinned_tabs=Pinned Tabs
tree.open_tabs=Open Tabs
tree.bookmarked_tabs=Bookmarked Tabs
+tree.filter=Tree filter
progress.load=Loading
progress.save_mappings=Saving mappings
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 5a1cca87bf0..47ff3c0fcd1 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
@@ -68,6 +68,7 @@ tree.loading=Cargando...
#tree.pinned_tabs=Pinned Tabs
#tree.open_tabs=Open Tabs
#tree.bookmarked_tabs=Bookmarked Tabs
+#tree.filter=Tree filter
progress.load=Cargando
#progress.save_mappings=Saving mappings
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 26278394fe1..23f4b81df05 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties
@@ -68,6 +68,7 @@ tree.loading=Memuat...
#tree.pinned_tabs=Pinned Tabs
#tree.open_tabs=Open Tabs
#tree.bookmarked_tabs=Bookmarked Tabs
+#tree.filter=Tree filter
progress.load=Memuat
progress.save_mappings=Menyimpan pemetaan
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 d289cbb340d..9ecc31aba88 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties
@@ -68,6 +68,7 @@ tree.loading=로딩중...
#tree.pinned_tabs=Pinned Tabs
#tree.open_tabs=Open Tabs
#tree.bookmarked_tabs=Bookmarked Tabs
+#tree.filter=Tree filter
progress.load=로딩중
progress.save_mappings=매핑 내보내는 중
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 feeae7290e9..e578cbce23c 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties
@@ -68,6 +68,7 @@ tree.loading=Carregando...
#tree.pinned_tabs=Pinned Tabs
#tree.open_tabs=Open Tabs
#tree.bookmarked_tabs=Bookmarked Tabs
+#tree.filter=Tree filter
progress.load=Carregando
#progress.save_mappings=Saving mappings
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 3827ff350a8..2b0c3b8d0b8 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties
@@ -68,6 +68,7 @@ tree.loading=Загрузка...
#tree.pinned_tabs=Pinned Tabs
#tree.open_tabs=Open Tabs
#tree.bookmarked_tabs=Bookmarked Tabs
+#tree.filter=Tree filter
progress.load=Загрузка
progress.save_mappings=Сохранить маппинги
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 b153850a73c..8db6b95248f 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
@@ -68,6 +68,7 @@ tree.loading=加载中…
tree.pinned_tabs=固定标签
tree.open_tabs=打开标签
tree.bookmarked_tabs=收藏标签
+#tree.filter=Tree filter
progress.load=正在加载
progress.save_mappings=导出映射
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 e260f80511d..196859217f8 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties
@@ -68,6 +68,7 @@ tree.loading=載入中...
tree.pinned_tabs=已釘選的分頁
tree.open_tabs=已開啟的分頁
tree.bookmarked_tabs=已加入書籤的分頁
+#tree.filter=Tree filter
progress.load=載入中
progress.save_mappings=正在匯出對應