Added: #2374 Quick filter by folder type (Ctrl+F on Resources view tag tree)

This commit is contained in:
Jindra Petřík
2025-01-17 21:47:12 +01:00
parent 867980eddd
commit 4621b181e4
9 changed files with 402 additions and 42 deletions
@@ -768,7 +768,8 @@ public final class ImagePanel extends JPanel implements MediaDisplay {
}
public void fireMediaDisplayStateChanged() {
for (MediaDisplayListener l : listeners) {
List<MediaDisplayListener> ls = new ArrayList<>(listeners);
for (MediaDisplayListener l : ls) {
l.mediaDisplayStateChanged(this);
}
}
@@ -405,7 +405,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se
private final JTabbedPane detailPanel;
private QuickTreeFindPanel quickTreeFindPanel;
private QuickTreeFilterPanel quickTreeFindPanel;
private QuickTreeFindPanel quickTagListFindPanel;
@@ -1827,7 +1827,8 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se
mainMenu.updateComponents(null);
previewPanel.clear();
dumpPreviewPanel.clear();
dumpPreviewPanel.clear();
doFilter();
return true;
}
@@ -1883,30 +1884,37 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se
return filter.trim().length() < 3;
}
private void doFilter(AbstractTagTree tree, QuickTreeFindPanel findPanel, List<List<String>> unfilteredExpandedNodes) {
private void doFilter(AbstractTagTree tree, QuickTreeFilterInterface findPanel, List<List<String>> unfilteredExpandedNodes) {
TreeModel model = tree.getModel();
String oldFilter = "";
List<String> oldFoldersFilter = new ArrayList<>();
if (model instanceof FilteredTreeModel) {
oldFilter = ((FilteredTreeModel) model).getFilter();
oldFoldersFilter = ((FilteredTreeModel) model).getFoldersFilter();
}
String newFilter = findPanel.getFilter();
List<String> newFoldersFilter = findPanel.getFolders();
if (isFilterEmpty(oldFilter)) {
unfilteredExpandedNodes.clear();
;
if (isFilterEmpty(oldFilter) && oldFoldersFilter.isEmpty()) {
unfilteredExpandedNodes.clear();
unfilteredExpandedNodes.addAll(View.getExpandedNodes(tree));
}
if (oldFilter.trim().equals(newFilter.trim())) {
if (
oldFilter.trim().equals(newFilter.trim())
&& oldFoldersFilter.equals(newFoldersFilter)
) {
return;
}
TreePath[] selectionPaths = tree.getSelectionPaths();
tree.setModel(new FilteredTreeModel(newFilter, tree.getFullModel(), tree));
tree.setModel(new FilteredTreeModel(newFilter, newFoldersFilter, tree.getFullModel(), tree));
if (!isFilterEmpty(newFilter)) {
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
} else if (!newFoldersFilter.isEmpty()) {
View.expandTreeNodes(tree, unfilteredExpandedNodes);
} else {
tree.setModel(tree.getFullModel());
View.expandTreeNodes(tree, unfilteredExpandedNodes);
@@ -1916,6 +1924,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se
public void doFilter() {
View.checkAccess();
quickTreeFindPanel.updateFolders();
doFilter(tagTree, quickTreeFindPanel, unfilteredTreeExpandedNodes);
doFilter(tagListTree, quickTagListFindPanel, unfilteredTagListExpandedNodes);
}
@@ -5647,7 +5656,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se
JPanel r = new JPanel(new BorderLayout());
r.add(resourcesClipboardPanel, BorderLayout.NORTH);
r.add(tagTreeScrollPanel = new FasterScrollPane(tagTree), BorderLayout.CENTER);
quickTreeFindPanel = new QuickTreeFindPanel();
quickTreeFindPanel = new QuickTreeFilterPanel(tagTree);
quickTreeFindPanel.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 JPEXS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jpexs.decompiler.flash.gui;
import java.util.List;
/**
*
* @author JPEXS
*/
public interface QuickTreeFilterInterface {
public String getFilter();
public List<String> getFolders();
}
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2022-2024 JPEXS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jpexs.decompiler.flash.gui;
import com.jpexs.decompiler.flash.gui.tagtree.TagTree;
import com.jpexs.decompiler.flash.gui.tagtree.TagTreeModel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* @author JPEXS
*/
public class QuickTreeFilterPanel extends JPanel implements QuickTreeFilterInterface {
private List<ActionListener> listeners = new ArrayList<>();
private JTextField filterField = new MyTextField("");
private final TagTree tagTree;
private JPanel foldersPanel;
private List<String> foldersList = new ArrayList<>();
private List<JLabel> folderLabelsList = new ArrayList<>();
private Map<JLabel, String> labelToFolder = new HashMap<>();
private Set<String> selectedFolders = new LinkedHashSet<>();
public QuickTreeFilterPanel(TagTree tagTree) {
this.tagTree = tagTree;
filterField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent e) {
warn();
}
@Override
public void removeUpdate(DocumentEvent e) {
warn();
}
@Override
public void insertUpdate(DocumentEvent e) {
warn();
}
public void warn() {
fireAction();
}
});
JPanel quickFindPanel = new JPanel(new BorderLayout(4, 0));
quickFindPanel.add(filterField, BorderLayout.CENTER);
quickFindPanel.add(new JLabel(View.getIcon("search16")), BorderLayout.WEST);
JLabel closeSearchButton = new JLabel(View.getIcon("cancel16"));
closeSearchButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
filterField.setText("");
setVisible(false);
fireAction();
}
});
closeSearchButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
quickFindPanel.add(closeSearchButton, BorderLayout.EAST);
setLayout(new BorderLayout());
add(quickFindPanel, BorderLayout.NORTH);
foldersPanel = new JPanel(new WrapLayout(WrapLayout.LEFT));
add(foldersPanel, BorderLayout.CENTER);
setVisible(false);
}
public void updateFolders() {
foldersPanel.removeAll();
folderLabelsList.clear();
foldersPanel.setSize(new Dimension(getParent().getWidth(), 47));
labelToFolder.clear();
foldersList = ((TagTreeModel) tagTree.getFullModel()).getAvailableFolders();
for (String f : foldersList) {
String icon = "folder" + f.toLowerCase(Locale.ENGLISH) + "16";
if (f.equals("header")) {
icon = "header16";
}
JLabel lab = new JLabel(AppStrings.translate("node." + f), View.getIcon(icon), JLabel.LEFT);
lab.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
labelToFolder.put(lab, f);
lab.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (selectedFolders.contains(f)) {
selectedFolders.remove(f);
} else {
selectedFolders.add(f);
}
labelHilight(lab, selectedFolders.contains(f));
fireAction();
}
});
folderLabelsList.add(lab);
foldersPanel.add(lab);
labelHilight(lab, selectedFolders.contains(f));
}
}
private Color fixColor(Color color) {
return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
}
private void labelHilight(JLabel label, boolean hilight) {
label.setOpaque(true);
if (hilight) {
label.setBackground(fixColor(UIManager.getColor("Tree.selectionBackground")));
label.setForeground(fixColor(UIManager.getColor("Tree.selectionForeground")));
} else {
label.setBackground(fixColor(UIManager.getColor("Tree.textBackground")));
label.setForeground(fixColor(UIManager.getColor("Tree.textForeground")));
}
label.repaint();
}
private void fireAction() {
for (ActionListener listener : listeners) {
listener.actionPerformed(new ActionEvent(this, 0, ""));
}
}
public void addActionListener(ActionListener listener) {
listeners.add(listener);
}
public void removeActionListener(ActionListener listener) {
listeners.remove(listener);
}
public String getFilter() {
return filterField.getText().trim();
}
@Override
public void setVisible(boolean aFlag) {
super.setVisible(aFlag);
if (aFlag) {
View.execInEventDispatchLater(new Runnable() {
@Override
public void run() {
updateFolders();
}
});
filterField.requestFocusInWindow();
} else {
filterField.setText("");
selectedFolders.clear();
}
}
@Override
public List<String> getFolders() {
return new ArrayList<>(selectedFolders);
}
}
@@ -33,7 +33,7 @@ import javax.swing.event.DocumentListener;
/**
* @author JPEXS
*/
public class QuickTreeFindPanel extends JPanel {
public class QuickTreeFindPanel extends JPanel implements QuickTreeFilterInterface {
private List<ActionListener> listeners = new ArrayList<>();
@@ -62,7 +62,7 @@ public class QuickTreeFindPanel extends JPanel {
}
});
setLayout(new BorderLayout());
setLayout(new BorderLayout(4, 0));
add(filterField, BorderLayout.CENTER);
add(new JLabel(View.getIcon("search16")), BorderLayout.WEST);
JLabel closeSearchButton = new JLabel(View.getIcon("cancel16"));
@@ -107,4 +107,8 @@ public class QuickTreeFindPanel extends JPanel {
}
}
@Override
public List<String> getFolders() {
return new ArrayList<>();
}
}
@@ -30,6 +30,7 @@ import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.gui.MainPanel;
import com.jpexs.decompiler.flash.gui.TreeNodeType;
import com.jpexs.decompiler.flash.gui.View;
import com.jpexs.decompiler.flash.gui.abc.ClassesListTreeModel;
import com.jpexs.decompiler.flash.gui.soleditor.Cookie;
import com.jpexs.decompiler.flash.iggy.conversion.IggySwfBundle;
import com.jpexs.decompiler.flash.tags.CSMTextSettingsTag;
@@ -183,6 +184,10 @@ public abstract class AbstractTagTree extends JTree {
return View.getIcon("foldersounds16");
}
if (val instanceof ClassesListTreeModel) {
return View.getIcon("folderscripts16");
}
TreeNodeType type = getTreeNodeType(val);
if (type == TreeNodeType.FOLDER && folderExpanded) {
@@ -18,6 +18,7 @@ package com.jpexs.decompiler.flash.gui.tagtree;
import com.jpexs.decompiler.flash.gui.abc.ClassesListTreeModel;
import com.jpexs.decompiler.flash.treeitems.FolderItem;
import com.jpexs.decompiler.flash.treeitems.HeaderItem;
import com.jpexs.decompiler.flash.treeitems.Openable;
import com.jpexs.decompiler.flash.treeitems.OpenableList;
import com.jpexs.decompiler.flash.treeitems.TreeItem;
@@ -37,6 +38,8 @@ import javax.swing.tree.TreePath;
public class FilteredTreeModel implements TreeModel {
private String filter;
private List<String> foldersFilter;
private TreeItem root;
private Map<TreeItem, List<TreeItem>> subItems = new WeakHashMap<>();
@@ -49,8 +52,13 @@ public class FilteredTreeModel implements TreeModel {
return filter;
}
public FilteredTreeModel(String filter, AbstractTagTreeModel fullModel, JTree tree) {
public List<String> getFoldersFilter() {
return foldersFilter;
}
public FilteredTreeModel(String filter, List<String> foldersFilter, AbstractTagTreeModel fullModel, JTree tree) {
this.filter = filter;
this.foldersFilter = foldersFilter;
this.tree = tree;
fullModel.addTreeModelListener(new TreeModelListener() {
@@ -114,13 +122,34 @@ public class FilteredTreeModel implements TreeModel {
private void buildTree(AbstractTagTreeModel fullModel, TreeItem item, String path, String searchPath, List<String> selectionPaths) {
List<? extends TreeItem> items = fullModel.getAllChildren(item);
List<TreeItem> newSubItems = new ArrayList<>();
if (filter.trim().isEmpty()) {
if (filter.trim().isEmpty() && foldersFilter.isEmpty()) {
newSubItems.addAll(items);
} else {
for (TreeItem ti : items) {
String subPath = path + "." + ti.toString();
String searchSubPath = isItemSearchable(ti) ? searchPath + "." + ti.toString() : searchPath;
boolean matches = searchSubPath.toLowerCase().contains(filter.toLowerCase());
if (!foldersFilter.isEmpty()) {
if (ti instanceof ClassesListTreeModel) {
if (!foldersFilter.contains("scripts")) {
continue;
}
}
if (ti instanceof FolderItem) {
FolderItem f = (FolderItem) ti;
if (!foldersFilter.contains(f.getName())) {
continue;
}
}
if (ti instanceof HeaderItem) {
HeaderItem h = (HeaderItem) ti;
if (!foldersFilter.contains("header")) {
continue;
}
}
}
if (fullModel.isLeaf(ti)) {
if (matches || selectionPaths.contains(subPath)) {
newSubItems.add(ti);
@@ -55,10 +55,13 @@ import com.jpexs.decompiler.flash.treeitems.OpenableList;
import com.jpexs.decompiler.flash.treeitems.TreeItem;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
@@ -70,7 +73,7 @@ import javax.swing.tree.TreePath;
public class TagTreeModel extends AbstractTagTreeModel {
public static final String FOLDER_COOKIES = "cookies";
public static final String FOLDER_SHAPES = "shapes";
public static final String FOLDER_MORPHSHAPES = "morphshapes";
@@ -99,6 +102,25 @@ public class TagTreeModel extends AbstractTagTreeModel {
public static final String FOLDER_SCENES = "scenes";
public static final List<String> FOLDERS_ORDER = Arrays.asList(
"header",
"cookies",
"shapes",
"morphshapes",
"sprites",
"texts",
"images",
"movies",
"sounds",
"buttons",
"fonts",
"binaryData",
"frames",
"scenes",
"others",
"scripts"
);
private final List<TreeModelListener> listeners = new ArrayList<>();
private final TagTreeRoot root = new TagTreeRoot();
@@ -282,7 +304,7 @@ public class TagTreeModel extends AbstractTagTreeModel {
}
}
}
List<TreeItem> cookies = new ArrayList<>();
if (swf.getFile() != null) {
List<File> solFiles = SharedObjectsStorage.getSolFilesForLocalFile(new File(swf.getFile()));
@@ -291,34 +313,69 @@ public class TagTreeModel extends AbstractTagTreeModel {
}
}
nodeList.add(new HeaderItem(swf, translate("node.header")));
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.cookies"), FOLDER_COOKIES, swf, cookies);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.shapes"), FOLDER_SHAPES, swf, shapes);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.morphshapes"), FOLDER_MORPHSHAPES, swf, morphShapes);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.sprites"), FOLDER_SPRITES, swf, sprites);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.texts"), FOLDER_TEXTS, swf, texts);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.images"), FOLDER_IMAGES, swf, images);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.movies"), FOLDER_MOVIES, swf, movies);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.sounds"), FOLDER_SOUNDS, swf, sounds);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.buttons"), FOLDER_BUTTONS, swf, buttons);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.fonts"), FOLDER_FONTS, swf, fonts);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.binaryData"), FOLDER_BINARY_DATA, swf, binaryData);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.frames"), FOLDER_FRAMES, swf, frames);
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.scenes"), FOLDER_SCENES, swf, scenes);
addFolderItem(nodeList, emptyFolders, true /*always add*/, translate("node.others"), FOLDER_OTHERS, swf, others);
Map<Tag, TagScript> currentTagScriptCache = new HashMap<>();
if (swf.isAS3()) {
if (!swf.getAbcList().isEmpty()) {
nodeList.add(new ClassesListTreeModel(swf, Configuration.flattenASPackages.get()));
}
} else {
List<TreeItem> subNodes = swf.getFirstLevelASMNodes(currentTagScriptCache);
if (subNodes.size() > 0) {
TreeItem actionScriptNode = new FolderItem(translate("node.scripts"), FOLDER_SCRIPTS, swf, subNodes);
nodeList.add(actionScriptNode);
for (String key : FOLDERS_ORDER) {
switch (key) {
case "header":
nodeList.add(new HeaderItem(swf, translate("node.header")));
break;
case "cookies":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.cookies"), FOLDER_COOKIES, swf, cookies);
break;
case "shapes":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.shapes"), FOLDER_SHAPES, swf, shapes);
break;
case "morphshapes":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.morphshapes"), FOLDER_MORPHSHAPES, swf, morphShapes);
break;
case "sprites":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.sprites"), FOLDER_SPRITES, swf, sprites);
break;
case "texts":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.texts"), FOLDER_TEXTS, swf, texts);
break;
case "images":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.images"), FOLDER_IMAGES, swf, images);
break;
case "movies":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.movies"), FOLDER_MOVIES, swf, movies);
break;
case "sounds":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.sounds"), FOLDER_SOUNDS, swf, sounds);
break;
case "buttons":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.buttons"), FOLDER_BUTTONS, swf, buttons);
break;
case "fonts":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.fonts"), FOLDER_FONTS, swf, fonts);
break;
case "binaryData":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.binaryData"), FOLDER_BINARY_DATA, swf, binaryData);
break;
case "frames":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.frames"), FOLDER_FRAMES, swf, frames);
break;
case "scenes":
addFolderItem(nodeList, emptyFolders, addAllFolders, translate("node.scenes"), FOLDER_SCENES, swf, scenes);
break;
case "others":
addFolderItem(nodeList, emptyFolders, true /*always add*/, translate("node.others"), FOLDER_OTHERS, swf, others);
break;
case "scripts":
if (swf.isAS3()) {
if (!swf.getAbcList().isEmpty()) {
nodeList.add(new ClassesListTreeModel(swf, Configuration.flattenASPackages.get()));
}
} else {
List<TreeItem> subNodes = swf.getFirstLevelASMNodes(currentTagScriptCache);
if (subNodes.size() > 0) {
TreeItem actionScriptNode = new FolderItem(translate("node.scripts"), FOLDER_SCRIPTS, swf, subNodes);
nodeList.add(actionScriptNode);
}
}
break;
}
}
@@ -330,6 +387,31 @@ public class TagTreeModel extends AbstractTagTreeModel {
swfInfos.put(swf, swfInfo);
}
public List<String> getAvailableFolders() {
Set<String> folderNames = new LinkedHashSet<>();
folderNames.add("header");
for (TagTreeSwfInfo swfInfo : swfInfos.values()) {
for (TreeItem item : swfInfo.folders) {
if (item instanceof FolderItem) {
FolderItem f = (FolderItem) item;
folderNames.add(f.getName());
}
if (item instanceof ClassesListTreeModel) {
folderNames.add("scripts");
}
}
}
List<String> ret = new ArrayList<>();
for (String f : FOLDERS_ORDER) {
if (folderNames.contains(f)) {
ret.add(f);
}
}
return ret;
}
private void addFolderItem(List<TreeItem> nodeList, List<FolderItem> emptyList, boolean addAllFolders, String title, String folderName, SWF swf, List<TreeItem> items) {
FolderItem node = new FolderItem(title, folderName, swf, items);
if (addAllFolders || !items.isEmpty()) {