Filtering on a JTree [closed]
Asked Answered
P

10

34

Problem

Applying filtering on a JTree to avoid certain nodes/leaves to show up in the rendered version of the JTree. Ideally I am looking for a solution which allows to have a dynamic filter, but I would already be glad if I can get a static filter to work.

To make it a bit easier, let us suppose the JTree only supports rendering, and not editing. Moving, adding, removing of nodes should be possible.

An example is a search field above a JTree, and on typing the JTree would only show the subtree with matches.

A few restrictions: it is to be used in a project which has access to JDK and SwingX. I would like to avoid to include other third party libs.

I already thought of a few possible solutions, but neither of those seemed ideal

Approaches

Model based filtering

  • decorate the TreeModel to filter out some of the values. A quick-and-dirt version is easy to write. Filter out nodes, and on every change of the filter or the delegate TreeModel the decorator can fire an event that the whole tree has changes (treeStructureChanged with the root node as node). Combine this with listeners which restore the selection state and the expansion state of the JTree and you get a version which works more or less, but the events originating from the TreeModel are messed up. This is more or less the approach used in this question
  • decorate the TreeModel but try fire the correct events. I did not (yet) managed to come up with a working version of this. It seems to require a copy of the delegate TreeModel in order to be able to fire an event with the correct child indices when nodes are removed from the delegate model. I think with some more time I could get this to work, but it just feels wrong (filtering feels like something the view should do, and not the model)
  • decorate whatever data structure was used to create the initial TreeModel. However, this is completely non-reusable, and probably as hard as to write a decorator for a TreeModel

View based filtering

This seems like the way to go. Filtering should not affect the model but only the view.

  • I took a look at RowFilter class. Although the javadoc seems to suggest you can use it in combination with a JTree:

    when associated with a JTree, an entry corresponds to a node.

    I could not find any link between RowFilter (or RowSorter) and the JTree class. The standard implementations of RowFilter and the Swing tutorials seems to suggest that RowFilter can only be used directly with a JTable (see JTable#setRowSorter). No similar methods are available on a JTree

  • I also looked at the JXTree javadoc. It has a ComponentAdapter available and the javadoc of ComponentAdapter indicates a RowFilter could interact with the target component, but I fail to see how I make the link between the RowFilter and the JTree
  • I did not yet look at how a JTable handles the filtering with RowFilters, and perhaps the same can be done on a modified version of a JTree.

So in short: I have no clue on what's the best approach to solve this

Note: this question is a possible duplicate of this question, but that question is still unanswered, the question rather short and the answers seems incomplete, so I thought to post a new question. If this is not done (the FAQ did not provide a clear answer on this) I will update that 3year old question

Preselector answered 10/2, 2012 at 20:19 Comment(13)
FYI: SwingX does not support sorting nor filtering of hierarchical structures. Actually, I started with sorting a couple of months ago which looks promising and should be applicable to filtering as well (didn't try that yet, though) Looking for sponsors to go ahead - hint, hint :-)Kursh
@Kursh any chance it will soon be included in SwingX ? Is your attempt online available so I can take a look at your approach ?Preselector
Sure, as soon as I can squeeze some funding from somewhere which allows me to work on it. No, nothing presentable yet (yeah, even I'm playing dirty when in private :-)Kursh
I have exactly the same problem now. And am quite discouraged to see so few answers on the topic......may I ask, have you decided on a solution?Mamie
@Mamie I did not spent much time on it after posting this question, as none of the solutions seemed good enough.Preselector
@Preselector understand. But i cant abandon the task, and should finish it asap. think ill go for smth like this, (just for reference, maybe it helps someone in future adrianwalker.org/2012/04/filtered-jtree.html . )Mamie
The idea described in #832313 is the easiest to implement, from my point of view. Also, I see no problem in calling a "filtered-model-subscribed-to-main-model-with-view" a "view": it behaves in the same way, so what's the difference?Lorrin
wondering about the functional/usability requirements: my current version (yeah, had a little bit time at hand :-) feels like filtering too much - it checks every node for inclusion: but if the parent isn't included it doesn't really matter if the down-under nodes match or not .. While I could technically add per-level conditions (like dont filter the root's direct children), I can't figure out what those conditions might be in practice.Kursh
@Kursh It is a tree, so when a parent is filtered out this automatically excludes the children as well. How on earth would you otherwise visualize that situation ? Bump the children up one level ? A disabled node (in which case you just need to adjust the renderer and do not need a filter). So I think your current approach is goodPreselector
no change of hierarchy, that would be gross :-) Just read in an older thread (ref by @Lorrin above) I was having trouble getting the Jide tree model to show the visible children of invisible parents plus jide now has an option leaf-only .. which probably is what s/he wanted at that time. Hmm ... back to thinking. ThanksKursh
@Kursh The JIDE option that I find is to hide the parent when none of the leafs match the filter (which is basically just another filter to apply). See setHideEmptyParentNode methodPreselector
the other properties are keepAllChildren and matchLeafOnly :-) Good idea to think of them like just-another-filter, that should work, thanksKursh
You can't easily implement a filter that removes empty parents because this isn't something you can do on an individual node and it becomes harder when you combine several filters. I succeeded in using Adrian Walker's code but there are still some cases in which it doesn't work, for example when using BFS and DFS implemented in DefaultMutableTreeNode.Woeful
R
9

Take a look at this implementation: http://www.java2s.com/Code/Java/Swing-Components/InvisibleNodeTreeExample.htm

It creates subclasses of DefaultMutableNode adding a "isVisible" property rather then actually removing/adding nodes from the TreeModel. Pretty sweet I think, and it solved my filtering problem neatly.

Ryter answered 14/12, 2012 at 13:31 Comment(7)
I think this is the neat working solutionDropsonde
This solution seems to be criminally overlooked. It is simple and clean, and doesn't have any weird side effects like zero-height rows.Collenecollet
I think, the given example (from 1999, wow!) does not consider adding new nodes while the tree is filtered. I get ArrayIndexOutOfBoundsException because of this.Flushing
I get ArrayIndexOutOfBoundsException when using model.nodeChanged or model.nodeStructureChanged. However, model.refresh seems to work, so I recommend to use this for triggering changes in the tree.Flushing
@Flushing What kind of TreeModel are you using that has a refresh() method? I don't see it in TreeModel or DefaultTreeModel. I've run into lots of AIOOBs with nodeStructureChanged so would like to try something else.Amaryllidaceous
@Amaryllidaceous I dont know anymore. Could be that I meant reload() instead of refresh(). I have not used Swing in some years now.Flushing
I tried that approach and it looked quite cool when I started, but I also found the behaviour can become buggy when adding/removing nodes, no exceptions but sometimes the wrong nodes become invisibile, will try the view-based approch instead now...Psychopathology
S
8

View-based filtering is definitely the way to go. You can use something like the example I've coded below. Another common practice when filtering trees is to switch to a list view when filtering a tree, since the list won't require you to show hidden nodes whose descendants need to be shown.

This is absolutely horrendous code (I tried to cut every corner possible in whipping it up just now), but it should be enough to get you started. Just type your query in the search box and press Enter, and it'll filter the JTree's default model. (FYI, the first 90 lines are just generated boilerplate and layout code.)

package com.example.tree;

import java.awt.BorderLayout;

public class FilteredJTreeExample extends JFrame {

    private JPanel contentPane;
    private JTextField textField;

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    FilteredJTreeExample frame = new FilteredJTreeExample();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public FilteredJTreeExample() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        JPanel panel = new JPanel();
        contentPane.add(panel, BorderLayout.NORTH);
        GridBagLayout gbl_panel = new GridBagLayout();
        gbl_panel.columnWidths = new int[]{34, 116, 0};
        gbl_panel.rowHeights = new int[]{22, 0};
        gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
        gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
        panel.setLayout(gbl_panel);

        JLabel lblFilter = new JLabel("Filter:");
        GridBagConstraints gbc_lblFilter = new GridBagConstraints();
        gbc_lblFilter.anchor = GridBagConstraints.WEST;
        gbc_lblFilter.insets = new Insets(0, 0, 0, 5);
        gbc_lblFilter.gridx = 0;
        gbc_lblFilter.gridy = 0;
        panel.add(lblFilter, gbc_lblFilter);

        JScrollPane scrollPane = new JScrollPane();
        contentPane.add(scrollPane, BorderLayout.CENTER);
        final JTree tree = new JTree();
        scrollPane.setViewportView(tree);

        textField = new JTextField();
        GridBagConstraints gbc_textField = new GridBagConstraints();
        gbc_textField.fill = GridBagConstraints.HORIZONTAL;
        gbc_textField.anchor = GridBagConstraints.NORTH;
        gbc_textField.gridx = 1;
        gbc_textField.gridy = 0;
        panel.add(textField, gbc_textField);
        textField.setColumns(10);
        textField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                TreeModel model = tree.getModel();
                tree.setModel(null);
                tree.setModel(model);
            }
        });

        tree.setCellRenderer(new DefaultTreeCellRenderer() {
            private JLabel lblNull = new JLabel();

            @Override
            public Component getTreeCellRendererComponent(JTree tree, Object value,
                    boolean arg2, boolean arg3, boolean arg4, int arg5, boolean arg6) {

                Component c = super.getTreeCellRendererComponent(tree, value, arg2, arg3, arg4, arg5, arg6);

                DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
                if (matchesFilter(node)) {
                    c.setForeground(Color.BLACK);
                    return c;
                }
                else if (containsMatchingChild(node)) {
                    c.setForeground(Color.GRAY);
                    return c;
                }
                else {
                    return lblNull;
                }
            }

            private boolean matchesFilter(DefaultMutableTreeNode node) {
                return node.toString().contains(textField.getText());
            }

            private boolean containsMatchingChild(DefaultMutableTreeNode node) {
                Enumeration<DefaultMutableTreeNode> e = node.breadthFirstEnumeration();
                while (e.hasMoreElements()) {
                    if (matchesFilter(e.nextElement())) {
                        return true;
                    }
                }

                return false;
            }
        });
    }

}

When you implement it for real, you'll probably want to create your own TreeNode and TreeCellRenderer implementations, use a less stupid method for triggering an update, and follow MVC separation. Note that the "hidden" nodes are still rendered, but they're so small that you can't see them. If you use the arrow keys to navigate the tree, though, you'll notice that they're still there. If you just need something that works, this might be good enough.

Filtered tree (windows)

Edit

Here are screenshots of the unfiltered and filtered version of the tree in Mac OS, showing that the whitespace is visible in Mac OS:

Unfiltered treeFiltered tree

Species answered 3/4, 2012 at 21:47 Comment(7)
I will check out your code this evening, but I am afraid that by returning an empty label for the filtered nodes things like root handles will still be shown for the filtered nodes. But I will check it out in more detail.Preselector
A nice attempt but the visual results are not good enough. You end up with a lot of empty space. I have included some screenshots to your answer to illustrate thisPreselector
Sorry it didn't work. The blank lines didn't show up for me; I guess the JRE implementation makes a difference. I've added my Windows screenshot. Does it work any better for you if you return a Canvas whose size is 0x0 instead of the empty JLabel?Species
+1 for the lateral thinking, even if it does not work too wellLorrin
What if you set the row height of invisible rows to 0?Covarrubias
It depends on the UI. I get the same empty spaces under Windows and probably under GNU Linux too.Woeful
@rob: After applying filter, mouse up scroll not working but down scroll works fine. Any idea?Shew
P
5

Old Question, I stumbled upon... for all those who want a quick and easy Solution of

JUST FILTERING THE VIEW:

I know it isn't as clean as Filtering the Model and comes with possible backdraws, but if you just want a quick solution for a small Application:

Extend the DefaultTableCellRenderer, override getTreeCellRendererComponent - invoke super.getTreeCellRendererComponent(...) and after that just set the Preferred Height to ZERO for all Nodes you want to hide. When constructing your JTree be sure to set setRowHeight(0); - so it will respect the Preferred Height of each row...

voila - all filtered Rows invisible!

COMPLETE WORKING EXAMPLE

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;

public class JTreeExample
{
    public static void main( final String[] args ) throws Exception
    {
        UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );

        // The only correct way to create a SWING Frame...
        EventQueue.invokeAndWait( new Runnable()
            {
                @Override
                public void run()
                {
                    swingMain();
                }
            } );
    }

    protected static void swingMain()
    {
        final JFrame f = new JFrame( "JTree Test" );
        f.setLocationByPlatform( true );
        f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

        final int items = 5;

        final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( "JTree", true );
        final DefaultTreeModel myModel = new DefaultTreeModel( rootNode );

        final Box buttonBox = new Box( BoxLayout.X_AXIS );

        for( int i = 0; i < items; i++ )
        {
            final String name = "Node " + i;
            final DefaultMutableTreeNode newChild = new DefaultMutableTreeNode( name );
            rootNode.add( newChild );

            final JButton b = new JButton( "Show/Hide " + i );
            buttonBox.add( b );
            b.addActionListener( new ActionListener()
                {
                    @Override
                    public void actionPerformed( final ActionEvent e )
                    {
                        // If the node has a Text, set it to null, otherwise reset it
                        newChild.setUserObject( newChild.getUserObject() == null ? name : null );
                        myModel.nodeStructureChanged( newChild.getParent() );
                    }
                } );
        }

        final JTree tree = new JTree( myModel );
        tree.setRowHeight( 0 );
        tree.setCellRenderer( new JTreeExample.TreeRenderer() );

        f.add( tree, BorderLayout.CENTER );
        f.add( buttonBox, BorderLayout.SOUTH );

        f.setSize( 600, 500 );
        f.setVisible( true );
    }

    public static class TreeRenderer extends DefaultTreeCellRenderer
    {
        @Override
        public Component getTreeCellRendererComponent( final JTree tree, final Object value, final boolean selected,
                                                        final boolean expanded, final boolean leaf, final int row, final boolean hasFocus )
        {
            // Invoke default Implementation, setting all values of this
            super.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus );

            if( !isNodeVisible( (DefaultMutableTreeNode)value ) )
            {
                setPreferredSize( new Dimension( 0, 0 ) );
            }
            else
            {
                setPreferredSize( new Dimension( 200, 15 ) );
            }

            return this;
        }
    }

    public static boolean isNodeVisible( final DefaultMutableTreeNode value )
    {
        // In this example all Nodes without a UserObject are invisible
        return value.getUserObject() != null;
    }
}
Palais answered 19/12, 2013 at 16:41 Comment(7)
It doesn't work, you just get some wholes like with all methods to filter the view including those returning an empty label and those using setRowHeight() to indicate that the row height is variable.Woeful
I have it up and running in an application I wrote... I will break down the code to a working minimal example if I find the time. If you setRowHeight() when constructing your JTree it will qurey each Node for its individual height and scale accordingly.Palais
That's what I did, I looked at the source code of Java, I saw that when the row height is greater than or equal to zero, it's considered as non constant. Therefore, I called setRowHeight(0) once and I set the preferred size to (0,0) in the method getTreeCellRendererComponent. The problem is that the management of variable sizes depends on the renderer too. I used the default UI under Windows and this trick didn't remove the blank space.Woeful
So - I have inserted a complete Working example - using the WindowsUI ;-)Palais
It works, thank you. Actually, I did something wrong, I set the minimal size, the maximum size and the size whereas the preferred size is picked by the renderer when the row height of the tree is set to zero. Big kudos to you :)Woeful
This method breaks mouse scrolling of JTree for me due to 0 height of nodesDropsonde
Use my Example - just wrap the tree into a new JScrollPane( tree ) on adding it and make the window smaller by dragging. Scrolling works perfectly fine. On Resize, on Filtering and on expanding. This is not a hack!!! Having individual Element Heights is a supported feature, 0 being a valid value.Palais
D
2

I've been working on a workarround for filtering an extended JXTreeTable. I've followed the two-model approach for simplicity.

The Filtered Model

public abstract class TellapicModelFilter extends DefaultTreeTableModel {

    protected Map<AbstractMutableTreeTableNode, 
                  AbstractMutableTreeTableNode>  family;
    protected Map<AbstractMutableTreeTableNode, 
                  AbstractMutableTreeTableNode>  filter;
    protected MyTreeTable                        treeTable;
    private   boolean                            withChildren;
    private   boolean                            withParents;

    /**
     * 
     * @param model
     */
    public TellapicModelFilter(MyTreeTable treeTable) {
        this(treeTable, false, false);
    }

    /**
    * 
    * @param treeTable
    * @param wp
    * @param wc
    */
    public TellapicModelFilter(MyTreeTable treeTable, boolean wp, boolean wc) {
        super(new DefaultMutableTreeTableNode("filteredRoot"));
        this.treeTable = treeTable;
        setIncludeChildren(wc);
        setIncludeParents(wp);
    }

    /**
     * 
     */
    public void filter() {
        filter = new HashMap<AbstractMutableTreeTableNode, AbstractMutableTreeTableNode>();
        family = new HashMap<AbstractMutableTreeTableNode, AbstractMutableTreeTableNode>();
        AbstractMutableTreeTableNode filteredRoot = (AbstractMutableTreeTableNode) getRoot();
        AbstractMutableTreeTableNode root = (AbstractMutableTreeTableNode) treeTable.getTreeTableModel().getRoot();
        filterChildren(root, filteredRoot);
        for(AbstractMutableTreeTableNode node : family.keySet())
            node.setParent(null);
        for(AbstractMutableTreeTableNode node : filter.keySet())
            node.setParent(filter.get(node));
    }

    /**
     * 
     * @param node
     * @param filteredNode
     */
    private void filterChildren(AbstractMutableTreeTableNode node, AbstractMutableTreeTableNode filteredNode) {
        int count = node.getChildCount();
        for(int i = 0; i < count; i++) {
            AbstractMutableTreeTableNode child = (AbstractMutableTreeTableNode) node.getChildAt(i);
            family.put(child, node);
            if (shouldBeFiltered(child)) {
                filter.put(child, filteredNode);
                if (includeChildren())
                    filterChildren(child, child);
            } else {
                filterChildren(child, filteredNode);
            }
        }
    }

    /**
     * 
     */
    public void restoreFamily() {
        for(AbstractMutableTreeTableNode child : family.keySet()) {
            AbstractMutableTreeTableNode parent = family.get(child);
            child.setParent(parent);
        }  
    }

    /**
     * 
     * @param node
     * @return
     */
    public abstract boolean shouldBeFiltered(AbstractMutableTreeTableNode node); 

    /**
     * Determines if parents will be included in the filtered result. This DOES NOT means that parent will be filtered
     * with the filter criteria. Instead, if a node {@code}shouldBeFiltered{@code} no matter what the parent node is,
     * include it in the filter result. The use of this feature is to provide contextual data about the filtered node,
     * in the terms of: "where was this node that belongs to?"
     * 
     * @return True is parents should be included anyhow.
     */
    public boolean includeParents() {
        return withParents;
    }

    /**
     * Determines if children should be filtered. When a node {@code}shouldBeFiltered{@code} you can stop the filtering
     * process in that node by setting: {@code}setIncludeChildren(false){@code}. In other words, if you want to filter
     * all the tree, {@code}includeChildren{@code} should return true.
     * 
     * By letting this method return {@code}false{@code} all children of the node filtered will be automatically added
     * to the resulting filter. That is, children aren't filtered with the filter criteria and they will be shown with
     * their parent in the filter result.
     * 
     * @return True if you want to filter all the tree.
     */
    public boolean includeChildren() {
        return withChildren;
    }

    /**
     * 
     * @param include
     */
    public void setIncludeParents(boolean include) {
       withParents = include;
    }

   /**
    * 
    * @param include
    */
   public void setIncludeChildren(boolean include) {
       withChildren = include;
   }

Basically, the idea was to connect/unconnect nodes from the original model to the filtered model root keeping track of the current family nodes.

The filtered model will have a mapping between children and parents, with the appropiate method to restore this family. The method ´restoreFamily´ will re-connect those missing childrens.

The filtered model will do most of the job in its filter() method leaving the abstract method shouldBeFiltered(node) to the implementations:

It should be taken into account, that there is no need to unconnect ALL children from family prior to connect the filtered ones to the filtered root. If performance is key, that behaviour could be analyzed more deeply.

Extending JXTreeTable

Lastly, but most important, there is the need to extend the underlying treetable by implementing one method and overriding another:

@Override
public void setTreeTableModel(TreeTableModel treeModel) {
    if (!(treeModel instanceof TellapicModelFilter))
        model = treeModel;

    super.setTreeTableModel(treeModel);
}

public void setModelFilter(TellapicModelFilter mf) {
    if (modelFilter != null) {
        modelFilter.restoreFamily();
        setTreeTableModel(getUnfilteredModel());
    }
    // Is this necessary?
    if (mf == null) {
        setTreeTableModel(getUnfilteredModel());
    } else {
        modelFilter = mf;
        modelFilter.filter();
        setTreeTableModel(modelFilter);
    }
}

A complete and working example with a treetable could be found at this link. It includes a Main.java with a ready-to-build tree. The testing GUI has a button that adds nodes in the selected node (if any) and in the top of the frame a text field that filters while writting.

Decease answered 2/7, 2012 at 16:9 Comment(2)
You are missing a "String text" in line Main.java:58. Also, from a UI point of view, having branches collapse and nodes reorder themselves while typing a query is not good.Lorrin
Thanks for pointing the missing variable. The Main.java contains an example that uses the filtering while writing (on caret update event). The reordering issue could be simply solved mantaining an ordered HashMap reflecting the current order that nodes are put. I would try to update the code thinking on that.Decease
E
2

ETable, a subclass of JTable and the parent class of Outline, described here, includes "Quick-Filter features allowing to show only certain rows from the model (see setQuickFilter())." While this violates the no "third party libs" requirement, the Outline JAR has no dependencies other than the JDK.

Exultant answered 4/6, 2013 at 11:47 Comment(8)
@Preselector TreeSort by aephyrFleck
curious: does that really work on the tree-aspect? That is filter correctly on the collapsed nodes? filtering the visible, expanded nodes is the easier part. Getting down to the hidden nodes (f.i. hiding the parent if none of its subtree matches) is the trickier part. In my own (not yet published SwingX addon) it requires to internally walk the complete tree (which might be costly)Kursh
@kleopatra: I'm not sure I understand; more here.Exultant
@Kursh did you tried, probably not there are both filter and sorterFleck
@Fleck my bad, didn't check, sorry :-)Kursh
taking the screenshot in your link as an example: what happens if you collapse the "outline" folder and type "FileDataProvider" in the quickfilter field?Kursh
I added a getQuickFilterPopup(2, 238, "test") menu item and filtered on <=; only nodes containing matching children remained. Choosing No filter restored the root node. I used the default transformValue(). Let me know if you want to tinker with my RowModel and RenderDataProvider.Exultant
It's useless for trees.Woeful
S
1

Here's a possible solution that uses only standard Swing components:

I haven't used this yet, but I like its implementation much more than other quick 'n dirty solutions I found online.

Salamis answered 3/4, 2012 at 22:10 Comment(5)
with the emphasis on dirty :-) Basically doing model-based filtering without proper notification, simply invalid ...Kursh
I must agree with @kleopatra. It would even be better to just replace my model by a new model each time the filter changes, which would at least trigger the correct eventsPreselector
I agree with both of you. I was going to implement this myself, but I decided to go with an entirely different UI instead of a filterable tree, otherwise I was going to report back with my experience using this implementation.Salamis
i wrote the component you are talking about. How is it invalid? It replaces the model everytime you filter. But if you don't have 1000s of elements its a perfectly acceptable solution.Marillin
Downvoted, for the same reason @Kursh et al already notedSeveralty
F
1

I've finally managed to pull of something the suits my needs perfectly, and thought I'd share in case someone else can use it.

I have been trying to display two JTrees side by side - one that contains a filtered list of the other.

Basically I have made two TreeModels, and use the same root node for both. This seems to work fine so far, so long as I make sure to override EVERY method that gets called from DefaultTreeModel in my code, such as nodeChanged( TreeNode node ) or else there will be pain.

The pain comes from the fact that the only time the nodes themselves are queried for information like childcount, is when nodestructure type methods are called on the DefaultTreeModel. Aside from that, all calls for tree structure information can be intercepted and filtered out as shown below.

This can get nasty as you have to make sure you dig out every time the nodes themselves are queried if you use DefaultTreeModel as the base like I did. This problem might not be there if you implement TreeModel directly instead of being lazy like me. NodesChanged source came straight from the JDK source.

I was lucky in that what I wanted meant there was always a path back to the root node from every item in the filtered list.

This was all I needed to do, even tho I spent the whole day trying wild and chaotic inventions, like recreating a shallow copy of the tree etc, not to mention reading lots on Stack !:

public class FilteredSceneModel extends DefaultTreeModel {

public static boolean isSceneItem(Object child) {
    return !(child instanceof DataItem);
}

public FilteredSceneModel(RootSceneNode root, SelectionModel sm) {
    super(root, sm);
}

private boolean isSceneFolder(Object node) {
    return node instanceof RootSceneNode || node instanceof Floor;
}

@Override
public AbstractSceneItem getChild(Object parent, int index) {
    AbstractSceneItem asi = (AbstractSceneItem) parent;
    if (isSceneItem(parent)) {
        int dex = 0;
        for (AbstractSceneItem child : asi.children) {
            if (isSceneItem(child)) {
                if (dex == index) {
                    return child;
                }
                dex++;
            }
        }
    }
    System.out.println("illegal state for: " + parent + " at index: " + index);
    return asi.getChildAt(index);
}

@Override
public int getChildCount(Object parent) {
    if (isSceneItem(parent)) {
        AbstractSceneItem asi = (AbstractSceneItem) parent;
        int count = 0;
        for (AbstractSceneItem child : asi.children) {
            if (isSceneItem(child)) {
                count++;
            }
        }
        return count;
    }
    return -1;
}

@Override
public int getIndexOfChild(Object parent, Object childItem) {
    if (isSceneItem(parent)) {
        AbstractSceneItem asi = (AbstractSceneItem) parent;
        int count = 0;
        for (AbstractSceneItem child : asi.children) {
            if (isSceneItem(child)) {
                if (child == childItem) {
                    return count;
                }
                count++;
            }
        }
    }
    return -1;
}

@Override
public boolean isLeaf(Object node) {
    if (isSceneItem(node)) {
        if (isSceneFolder(node)) {
            return false;
        }
    }
    return true;
}

@Override
public void activeFloorChanged(Floor floor) {
    for (AbstractSceneItem asi : floor) {
        if (isSceneItem(asi)) {
            nodeChanged(asi);
        }
    }
}

@Override
protected void renamed(AbstractSceneItem asi) {
    if (isSceneItem(asi)) {
        nodeChanged(asi);
        System.out.println("scene only model renamed: " + asi.fullPathToString());
    }
}

@Override
public void nodeChanged(TreeNode tn) {
    if (isSceneItem(tn)) {
        filteredNodeChanged(tn);
    }
}

@Override
public void nodeStructureChanged(TreeNode tn) {
    if (isSceneItem(tn)) {
        super.nodeStructureChanged(tn);
    }
}

private void filteredNodeChanged(TreeNode node) {
    if (listenerList != null && node != null) {
        TreeNode parent = node.getParent();

        if (parent != null) {
            int anIndex = getIndexOfChild(parent, node);
            if (anIndex != -1) {
                int[] cIndexs = new int[1];

                cIndexs[0] = anIndex;
                nodesChanged(parent, cIndexs);
            }
        } else if (node == getRoot()) {
            nodesChanged(node, null);
        }
    }
}

@Override
public void nodesChanged(TreeNode node, int[] childIndices) {
    if (node != null) {
        if (childIndices != null) {
            int cCount = childIndices.length;

            if (cCount > 0) {
                Object[] cChildren = new Object[cCount];

                for (int counter = 0; counter < cCount; counter++) {
                    cChildren[counter] = getChild(node, childIndices[counter]);
                }
                fireTreeNodesChanged(this, getPathToRoot(node),
                        childIndices, cChildren);
            }
        } else if (node == getRoot()) {
            fireTreeNodesChanged(this, getPathToRoot(node), null, null);
        }
    }
}
}
Fecal answered 15/8, 2012 at 10:1 Comment(0)
S
0

Principle that i used: Filling ArrayList from DB, then populating tree. When i have to filter tree nodes, i just iterate through ArrayList, remove all nodes that don't match criteria, and then rebuild the tree with modified ArrayList...

Slow answered 11/6, 2012 at 13:0 Comment(0)
D
0

I have a suggestion for this which may be of interest. I've put it into practice in my own app and it seems to work nicely... below is an absolute minimal implementation SSCCE demonstrating "insertNodeInto".

The central design is multiple couplings of JTree-TreeModel which are all kept in perfect synch with one another... except, obviously, that some filtering pattern is applied so that certain nodes (and their subtrees) are not present in one model. Meanwhile each node in the ON tree has a "counterpart" node in the OFF tree (although the converse is not necessarily true).

The simplest design therefore involves 2 such couplings: one with the filter "OFF" and the other with the filter "ON" (incidentally you can have more than 1 filter, so that you then need n^2 couplings, where n is the number of filters... and I have got this working!).

To switch from one coupling to another (i.e. from ON to OFF or vice versa) you just substitute one JTree for another in the containing JViewport... and this happens in the blink of an eye, completely unspottable. So it's kind of like an optical illusion.

By the way, the filter used here is "does the toString() of the node contain the string 'nobble'"? (see method FilterPair.is_filtered_out)

Some might say that such an idea would be ridiculously memory inefficient... but in fact the nodes in the different couplings, although different nodes, use the same user object... so I suggest the structure is fairly lightweight.

Much, much more difficult is getting the mechanics of two couplings (let alone 4 or 8) to synch up to one another. Below I demonstrate a fairly comprehensive implementation of insertNodeInto... but many methods of DefaultTreeModel, of JTree, and also relating to selection, require a lot of thought. E.g. if selection in the (filter) OFF tree is on a node which has no counterpart in the ON tree (because it or one of its ancestors has been filtered out), where should selection in the ON tree go? I have found answers to all of these questions but there isn't space here to show them...

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.tree.*;

public class FilterTreeDemo {
  public static void main(String[] args) throws FileNotFoundException {
    EventQueue.invokeLater(new ShowIt());
  }
}

class FiltNode extends DefaultMutableTreeNode { 
  FiltNode( Object user_obj ){
    super( user_obj );
  }
  FiltNode m_counterpart_node;

//  public String toString(){
//    // hash code demonstrates (as you toggle) that these are not the same nodes...
//    return super.toString() + " (" + hashCode() + ")"; 
//  }
}

class FilterPair {

  TreeCoupling m_on_coupling, m_off_coupling;
  boolean m_filter_on = true;
  JFrame m_main_frame;
  FiltNode m_on_root = new FiltNode( "root" );
  FiltNode m_off_root = new FiltNode( "root" );

  // needed to prevent infinite calling between models...  
  boolean m_is_propagated_call = false;

  FilterPair( JFrame main_frame ){
    m_on_root.m_counterpart_node = m_off_root;
    m_off_root.m_counterpart_node = m_on_root;
    m_on_coupling = new TreeCoupling( true ); 
    m_off_coupling = new TreeCoupling( false );
    m_main_frame = main_frame;
    // starts by toggling to OFF (i.e. before display)
    toggle_filter();
  }

  // this is the filter method for this particular FilterPair...
  boolean is_filtered_out( MutableTreeNode node ){
    return node.toString().contains( "nobble");
  }


  class TreeCoupling {


    class FilterTreeModel extends DefaultTreeModel {
      FilterTreeModel( TreeNode root ){
        super( root );
      }

      public void insertNodeInto(MutableTreeNode new_child, MutableTreeNode parent, int index){
        // aliases for convenience
        FiltNode new_filt_node = (FiltNode)new_child;
        FiltNode parent_filt_node = (FiltNode)parent;

        FiltNode new_counterpart_filt_node = null;
        FiltNode counterpart_parent_filt_node = null;
        // here and below the propagation depth test is used to skip code which is leading to another call to 
        // insertNodeInto on the counterpart TreeModel...
        if( ! m_is_propagated_call ){
          // NB the counterpart new FiltNode is given exactly the same user object as its counterpart: no duplication
          // of the user object...
          new_counterpart_filt_node = new FiltNode( new_filt_node.getUserObject() );
          counterpart_parent_filt_node = parent_filt_node.m_counterpart_node;
          // set up the 2 counterpart relationships between the node in the ON tree and the node in the OFF tree
          new_counterpart_filt_node.m_counterpart_node = new_filt_node;
          new_filt_node.m_counterpart_node = new_counterpart_filt_node;
        }

        if( TreeCoupling.this == m_on_coupling ){
          // ... we are in the ON coupling

          // if first call and the parent has no counterpart (i.e. in the OFF coupling) sthg has gone wrong
          if( ! m_is_propagated_call && counterpart_parent_filt_node == null ){
            throw new NullPointerException();
          }
          if( ! is_filtered_out( new_filt_node ) ){
            // only insert here (ON coupling) if the node is NOT filtered out...
            super.insertNodeInto( new_filt_node, parent_filt_node, index);
          }
          else {
            // enable the originally submitted new node (now rejected) to be unlinked and garbage-collected...
            // (NB if you suspect the first line here is superfluous, try commenting out and see what happens)
            new_filt_node.m_counterpart_node.m_counterpart_node = null;
            new_filt_node.m_counterpart_node = null;
          }
          if( ! m_is_propagated_call  ){
            // as we are in the ON coupling we can't assume that the index value should be passed on unchanged to the
            // OFF coupling: some siblings (of lower index) may be missing here... but we **do** know that the previous 
            // sibling (if there is one) of the new node has a counterpart in the OFF tree... so get the index of its
            // OFF counterpart and add 1...
            int off_index = 0;
            if( index > 0 ){
              FiltNode prev_sib = (FiltNode)parent_filt_node.getChildAt( index - 1 );
              off_index = counterpart_parent_filt_node.getIndex( prev_sib.m_counterpart_node ) + 1;
            }
            m_is_propagated_call = true;
            m_off_coupling.m_tree_model.insertNodeInto( new_counterpart_filt_node, counterpart_parent_filt_node, off_index);
          }

        }
        else {
          // ... we are in the OFF coupling

          super.insertNodeInto( new_filt_node, parent_filt_node, index);
          if( ! m_is_propagated_call  ){

            // we are in the OFF coupling: it is perfectly legitimate for the parent to have no counterpart (i.e. in the 
            // ON coupling: indicates that it, or an ancestor of it, has been filtered out)
            if( counterpart_parent_filt_node != null ){
              // OTOH, if the parent **is** available, we can't assume that the index value should be passed on unchanged: 
              // some siblings of the new incoming node (of lower index) may have been filtered out... to find the 
              // correct index value we track down the index value until we reach a node which has a counterpart in the 
              // ON coupling... or if not found the index must be 0 
              int on_index = 0;
              if( index > 0 ){
                for( int i = index - 1; i >= 0; i-- ){
                  FiltNode counterpart_sib = ((FiltNode)parent_filt_node.getChildAt( i )).m_counterpart_node;
                  if( counterpart_sib != null ){
                    on_index = counterpart_parent_filt_node.getIndex( counterpart_sib ) + 1;
                    break;
                  }
                }
              }
              m_is_propagated_call = true;
              m_on_coupling.m_tree_model.insertNodeInto( new_counterpart_filt_node, counterpart_parent_filt_node, on_index);
            }
            else {
              // ... no ON-coupling parent node "counterpart": the new ON node must be discarded  
              new_filt_node.m_counterpart_node = null;
            }


          }
        }
        m_is_propagated_call = false;
      }
    }

    JTree m_tree;
    FilterTreeModel m_tree_model;
    TreeCoupling( boolean on ){
      m_tree = new JTree();
      m_tree_model = on ? new FilterTreeModel( m_on_root ) : new FilterTreeModel( m_off_root ); 
      m_tree.setModel( m_tree_model );
    }
  }

   void toggle_filter(){
    m_filter_on = ! m_filter_on;
    m_main_frame.setTitle( m_filter_on? "FilterTree - ON (Ctrl-F6 to toggle)" : "FilterTree - OFF (Ctrl-F6 to toggle)" ); 
  }

  TreeCoupling getCurrCoupling(){
    return m_filter_on? m_on_coupling : m_off_coupling;
  }
}


class ShowIt implements Runnable {
  @Override
  public void run() {
    JFrame frame = new JFrame("FilterTree");
    final FilterPair pair = new FilterPair( frame ); 
    final JScrollPane jsp = new JScrollPane( pair.getCurrCoupling().m_tree );
    Action toggle_between_views = new AbstractAction( "toggle filter" ){
      @Override
      public void actionPerformed(ActionEvent e) {
        pair.toggle_filter();
        jsp.getViewport().setView( pair.getCurrCoupling().m_tree );
        jsp.requestFocus();
      }};
    JPanel cpane = (JPanel)frame.getContentPane(); 
    cpane.getActionMap().put("toggle between views", toggle_between_views );
    InputMap new_im = new InputMap();
    new_im.put(KeyStroke.getKeyStroke(KeyEvent.VK_F6, InputEvent.CTRL_DOWN_MASK), "toggle between views");
    cpane.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, new_im);

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().add(jsp);
    frame.pack();
    frame.setBounds(50, 50, 800, 500);
    frame.setVisible(true);

    // populate the tree(s) NB we are currently viewing the OFF tree
    FilterPair.TreeCoupling curr_coupling = pair.getCurrCoupling(); 
    curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 1" ), (FiltNode)curr_coupling.m_tree_model.getRoot(), 0 );
    FiltNode d2 = new FiltNode( "scrags 2" );
    curr_coupling.m_tree_model.insertNodeInto( d2, (FiltNode)curr_coupling.m_tree_model.getRoot(), 1 );
    curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 3" ), (FiltNode)curr_coupling.m_tree_model.getRoot(), 2 );
    curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 2.1" ), d2, 0 );

    // this will be filtered out of the ON tree
    FiltNode nobble = new FiltNode( "nobble" );
    curr_coupling.m_tree_model.insertNodeInto( nobble, d2, 1 );
    // this will also be filtered out of the ON tree
    FiltNode son_of_nobble = new FiltNode( "son of nobble");
    curr_coupling.m_tree_model.insertNodeInto( son_of_nobble, nobble, 0 );    

    curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "peewit (granddaughter of n****e)"), son_of_nobble, 0 );    

    // expand the OFF tree
    curr_coupling.m_tree.expandPath( new TreePath( curr_coupling.m_tree_model.getRoot() ) );
    curr_coupling.m_tree.expandPath( new TreePath( d2.getPath() ) );
    curr_coupling.m_tree.expandPath( new TreePath( nobble.getPath() ) );
    curr_coupling.m_tree.expandPath( new TreePath( son_of_nobble.getPath() ) );

    // switch view (programmatically) to the ON tree
    toggle_between_views.actionPerformed( null );

    // expand the ON tree
    curr_coupling = pair.getCurrCoupling();
    curr_coupling.m_tree.expandPath( new TreePath( curr_coupling.m_tree_model.getRoot() ) );
    curr_coupling.m_tree.expandPath( new TreePath( d2.m_counterpart_node.getPath() ) );

    // try to expand the counterpart of "nobble"... there shouldn't be one...
    FiltNode nobble_counterpart = nobble.m_counterpart_node;
    if( nobble_counterpart != null ){
      curr_coupling.m_tree.expandPath( new TreePath( nobble_counterpart.getPath() ) );
      System.err.println( "oops..." );
    }
    else {
      System.out.println( "As expected, node \"nobble\" has no counterpart in the ON coupling" );
    }



    // try inserting a node into the ON tree which will immediately be "rejected" by the ON tree (due to being 
    // filtered out before the superclass insertNodeInto is called), but will nonetheless appear in the 
    // OFF tree as it should...
    curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "yet another nobble"), d2.m_counterpart_node, 0 );


  }
}

One more thought: how to generalise it so that FilterTreeModel extends your own supplied subclass of DefaultTreeModel? (and in the full implmentation so that FilterJTree extends your supplied JTree subclass?). I wrote this code originally in Jython, where passing a class A as a parameter of a definition of class B is trivial! Using stately old Java, it could possibly be done with reflection and static factory methods, or possibly by some ingenious encapsulation technique. It'd be a hard slog though. Better to switch to Jython if at all possible!

Driftage answered 27/10, 2013 at 12:47 Comment(0)
E
-1

A solution was given http://forums.sun.com/thread.jspa?forumID=57&threadID=5378510

We have implemented it here, adn it works as a charm.

You just have to implement you TreeModel, and on the JTree use a FilterTreeModel with a TreeFilter.

The implementation is quite simple, there is perhaps maybe some stuff to do on listener, since the actual code will called twice, which is not good at all. My idea is to just pass listener to delegate model, I don't see the point to add listener on the filter model... But that is another question.

Expansile answered 3/1, 2018 at 19:10 Comment(2)
That implementation looks incorrect. For example a listener added to the FilterTreeModel will receive events from the delegate model. Not only is the source tree model unexpected, the events might also contain indices which are simply not available on FilterTreeModel.Preselector
@Robin, yes the implementation can not be perfect since the delegate model can have others listeners. I just change codes to always use delegate models listeners (and populate them if possible). I know that you can not use this code for general purpose, but if you control exactly listeners of delegate model I works for me very well.Expansile

© 2022 - 2024 — McMap. All rights reserved.