All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.extended.tree.DefaultTreeCheckingModel Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library 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.
 *
 * WebLookAndFeel library 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 WebLookAndFeel library.  If not, see .
 */

package com.alee.extended.tree;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.laf.checkbox.CheckState;
import com.alee.laf.tree.NodesAcceptPolicy;

import javax.swing.event.EventListenerList;
import javax.swing.tree.MutableTreeNode;
import java.util.*;

/**
 * Default checking model for {@link WebCheckBoxTree}.
 *
 * @param  {@link MutableTreeNode} type
 * @param  {@link WebCheckBoxTree} type
 * @author Mikle Garin
 */
public class DefaultTreeCheckingModel> implements TreeCheckingModel
{
    /**
     * {@link WebCheckBoxTree} that uses this checking model.
     */
    @NotNull
    protected final T checkBoxTree;

    /**
     * {@link MutableTreeNode} check states.
     */
    @NotNull
    protected Map nodeCheckStates;

    /**
     * Model listeners.
     * todo Remove and simply fire this through {@link #checkBoxTree}.
     *
     * @see CheckStateChangeListener
     */
    @NotNull
    protected EventListenerList listeners;

    /**
     * Comparator for {@link MutableTreeNode}s returned for specified {@link CheckState}.
     */
    @NotNull
    protected Comparator nodesComparator;

    /**
     * Constructs new {@link DefaultTreeCheckingModel} for the specified {@link WebCheckBoxTree}.
     *
     * @param checkBoxTree {@link WebCheckBoxTree} that uses this {@link DefaultTreeCheckingModel}
     */
    public DefaultTreeCheckingModel ( @NotNull final T checkBoxTree )
    {
        this.checkBoxTree = checkBoxTree;
        this.nodeCheckStates = new WeakHashMap ();
        this.listeners = new EventListenerList ();
        this.nodesComparator = createNodesComparator ();
    }

    /**
     * Returns new {@link Comparator} for nodes.
     * Can also return {@code null} to disable nodes sorting.
     *
     * @return new {@link Comparator} for nodes
     */
    @NotNull
    protected Comparator createNodesComparator ()
    {
        return new NodesPositionComparator ();
    }

    @NotNull
    @Override
    public List getNodes ( @NotNull final CheckState state, @NotNull final NodesAcceptPolicy policy )
    {
        // Collecting nodes for state
        final List collected = new ArrayList ();
        if ( state == CheckState.checked || state == CheckState.mixed )
        {
            // Collecting checked or mixed nodes
            for ( final Map.Entry entry : nodeCheckStates.entrySet () )
            {
                if ( entry.getValue () == state )
                {
                    collected.add ( entry.getKey () );
                }
            }
        }
        else
        {
            // Collecting unchecked nodes
            final List runthrough = new ArrayList ();
            runthrough.add ( checkBoxTree.getRootNode () );
            while ( !runthrough.isEmpty () )
            {
                // Removing first element to shrink runthrough list
                final N node = runthrough.remove ( 0 );
                final CheckState nodeState = nodeCheckStates.get ( node );
                final boolean unchecked = nodeState == null || nodeState == CheckState.unchecked;

                // Simply adding unchecked node
                if ( unchecked )
                {
                    collected.add ( node );
                }

                // Make sure to check all child nodes
                for ( int i = 0; i < getChildCount ( node ); i++ )
                {
                    runthrough.add ( getChildAt ( node, i ) );
                }
            }
        }

        // Removing chilren of collected nodes
        policy.filter ( checkBoxTree, collected );

        // Sorting nodes by their position in the tree
        Collections.sort ( collected, nodesComparator );

        return collected;
    }

    @NotNull
    @Override
    public CheckState getCheckState ( @NotNull final N node )
    {
        final CheckState checkState = nodeCheckStates.get ( node );
        return checkState != null ? checkState : CheckState.unchecked;
    }

    @Override
    public void setChecked ( @NotNull final N node, final boolean checked )
    {
        // Collecting state changes
        final boolean collectChanges = listeners.getListenerCount ( CheckStateChangeListener.class ) > 0;
        List> changes = null;
        if ( collectChanges )
        {
            changes = new ArrayList> ( 1 );
        }

        // Updating states
        final List toUpdate = new ArrayList ();
        setCheckedImpl ( node, checked, toUpdate, changes );
        repaintTreeNodes ( toUpdate );

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    @Override
    public void setChecked ( @NotNull final Collection nodes, final boolean checked )
    {
        // Collecting state changes
        final boolean collectChanges = listeners.getListenerCount ( CheckStateChangeListener.class ) > 0;
        List> changes = null;
        if ( collectChanges )
        {
            changes = new ArrayList> ( nodes.size () );
        }

        // Updating states
        final List toUpdate = new ArrayList ();
        for ( final N node : nodes )
        {
            setCheckedImpl ( node, checked, toUpdate, changes );
        }
        repaintTreeNodes ( toUpdate );

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    /**
     * Sets whether the specified tree node is checked or not.
     *
     * @param node     tree node to process
     * @param checked  whether the specified tree node is checked or not
     * @param toUpdate list of nodes for later update
     * @param changes  list to collect state changes into
     */
    protected void setCheckedImpl ( @NotNull final N node, final boolean checked, @NotNull final List toUpdate,
                                    @Nullable final List> changes )
    {
        // Remembering old and new states
        final CheckState oldState = getCheckState ( node );
        final CheckState newState = checked ? CheckState.checked : CheckState.unchecked;

        // Updating node if check state has actually changed
        if ( oldState != newState )
        {
            // Changing node check state
            updateNodeState ( node, newState, toUpdate );

            // Saving changes
            if ( changes != null )
            {
                changes.add ( new CheckStateChange ( node, oldState, newState ) );
            }

            // Updating parent and child node states
            if ( checkBoxTree.isRecursiveCheckingEnabled () )
            {
                updateChildNodesState ( node, newState, toUpdate, changes );
                updateParentStates ( node, toUpdate, changes );
            }
        }
    }

    /**
     * Updates parent nodes check states.
     *
     * @param node     node to start checking parents from
     * @param toUpdate list of nodes for later update
     * @param changes  list to collect state changes into
     */
    protected void updateParentStates ( @NotNull final N node, @NotNull final List toUpdate,
                                        @Nullable final List> changes )
    {
        // Updating all parent node states
        N parent = getParent ( node );
        while ( parent != null )
        {
            // Calculating parent state
            CheckState state = CheckState.unchecked;
            boolean hasChecked = false;
            boolean hasUnchecked = false;
            for ( int i = 0; i < getChildCount ( parent ); i++ )
            {
                final CheckState checkState = getCheckState ( getChildAt ( parent, i ) );
                if ( checkState == CheckState.mixed )
                {
                    state = CheckState.mixed;
                    break;
                }
                else if ( checkState == CheckState.checked )
                {
                    hasChecked = true;
                    if ( hasUnchecked )
                    {
                        state = CheckState.mixed;
                        break;
                    }
                    else
                    {
                        state = CheckState.checked;
                    }
                }
                else if ( checkState == CheckState.unchecked )
                {
                    hasUnchecked = true;
                    if ( hasChecked )
                    {
                        state = CheckState.mixed;
                        break;
                    }
                    else
                    {
                        state = CheckState.unchecked;
                    }
                }
            }

            final CheckState oldState = getCheckState ( parent );
            if ( oldState != state )
            {
                // Saving changes
                if ( changes != null )
                {
                    changes.add ( new CheckStateChange ( parent, oldState, state ) );
                }

                // Updating state
                updateNodeState ( parent, state, toUpdate );
            }

            // Moving upstairs
            parent = getParent ( parent );
        }
    }

    /**
     * Updates child nodes check state.
     *
     * @param node     parent node
     * @param newState new check state
     * @param toUpdate list of nodes for later update
     * @param changes  list to collect state changes into
     */
    protected void updateChildNodesState ( @NotNull final N node, @NotNull final CheckState newState, @NotNull final List toUpdate,
                                           @Nullable final List> changes )
    {
        for ( int i = 0; i < getChildCount ( node ); i++ )
        {
            final N childNode = getChildAt ( node, i );

            // Saving changes
            if ( changes != null )
            {
                changes.add ( new CheckStateChange ( childNode, getCheckState ( childNode ), newState ) );
            }

            // Updating state
            updateNodeState ( childNode, newState, toUpdate );

            // Updating child nodes state
            updateChildNodesState ( childNode, newState, toUpdate, changes );
        }
    }

    /**
     * Updates single node check state.
     *
     * @param node     node to update
     * @param newState new check state
     * @param toUpdate list of nodes for later update
     */
    protected void updateNodeState ( @NotNull final N node, @NotNull final CheckState newState, @NotNull final List toUpdate )
    {
        if ( newState != CheckState.unchecked )
        {
            nodeCheckStates.put ( node, newState );
        }
        else
        {
            nodeCheckStates.remove ( node );
        }
        toUpdate.add ( node );
    }

    @Override
    public void invertCheck ( @NotNull final N node )
    {
        // Collecting state changes
        final boolean collectChanges = listeners.getListenerCount ( CheckStateChangeListener.class ) > 0;
        List> changes = null;
        if ( collectChanges )
        {
            changes = new ArrayList> ( 1 );
        }

        // Updating states
        final List toUpdate = new ArrayList ();
        setCheckedImpl ( node, getNextState ( getCheckState ( node ) ) == CheckState.checked, toUpdate, changes );
        repaintTreeNodes ( toUpdate );

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    @Override
    public void invertCheck ( @NotNull final Collection nodes )
    {
        // Collecting state changes
        final boolean collectChanges = listeners.getListenerCount ( CheckStateChangeListener.class ) > 0;
        List> changes = null;
        if ( collectChanges )
        {
            changes = new ArrayList> ( nodes.size () );
        }

        // Updating states
        final List toUpdate = new ArrayList ();
        boolean check = false;
        for ( final N node : nodes )
        {
            if ( getCheckState ( node ) != CheckState.checked )
            {
                check = true;
                break;
            }
        }
        for ( final N node : nodes )
        {
            setCheckedImpl ( node, check, toUpdate, changes );
        }
        repaintTreeNodes ( toUpdate );

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    @Override
    public void checkAll ()
    {
        final List allNodes = checkBoxTree.getAvailableNodes ();

        // Collecting state changes
        List> changes = null;
        if ( listeners.getListenerCount ( CheckStateChangeListener.class ) > 0 )
        {
            changes = new ArrayList> ( allNodes.size () );
            for ( final N node : allNodes )
            {
                final CheckState state = getCheckState ( node );
                if ( state != CheckState.checked )
                {
                    changes.add ( new CheckStateChange ( node, state, CheckState.checked ) );
                }
            }
        }

        // Updating states
        for ( final N node : allNodes )
        {
            nodeCheckStates.put ( node, CheckState.checked );
        }
        repaintVisibleTreeRect ();

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    @Override
    public void uncheckAll ()
    {
        // Collecting state changes
        List> changes = null;
        if ( listeners.getListenerCount ( CheckStateChangeListener.class ) > 0 )
        {
            changes = new ArrayList> ( nodeCheckStates.size () );
            for ( final Map.Entry entry : nodeCheckStates.entrySet () )
            {
                final CheckState state = entry.getValue ();
                if ( state == CheckState.mixed || state == CheckState.checked )
                {
                    changes.add ( new CheckStateChange ( entry.getKey (), state, CheckState.unchecked ) );
                }
            }
        }

        // Updating states
        nodeCheckStates.clear ();
        repaintVisibleTreeRect ();

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    /**
     * Returns next check state for check invertion action.
     *
     * @param checkState current check state
     * @return next check state for check invertion action
     */
    @NotNull
    protected CheckState getNextState ( @NotNull final CheckState checkState )
    {
        final CheckState nextState;
        switch ( checkState )
        {
            case unchecked:
                nextState = CheckState.checked;
                break;

            case mixed:
                nextState = checkBoxTree.isCheckMixedOnToggle () ? CheckState.checked : CheckState.unchecked;
                break;

            case checked:
            default:
                nextState = CheckState.unchecked;
                break;
        }
        return nextState;
    }

    @Override
    public void checkingModeChanged ( final boolean recursive )
    {
        // Collecting state changes
        final boolean collectChanges = listeners.getListenerCount ( CheckStateChangeListener.class ) > 0;
        List> changes = null;
        if ( collectChanges )
        {
            changes = new ArrayList> ();
        }

        // Updating states
        final List toUpdate = new ArrayList ();
        if ( recursive )
        {
            // Retrieving all checked nodes
            final List checked = new ArrayList ( nodeCheckStates.size () );
            for ( final Map.Entry entry : nodeCheckStates.entrySet () )
            {
                if ( entry.getValue () == CheckState.checked )
                {
                    checked.add ( entry.getKey () );
                }
            }

            // Filtering out child nodes under other checked nodes
            // We don't need to update them because they will get checked in the process
            filterOutChildNodes ( checked );

            // Updating node states
            for ( final N node : checked )
            {
                updateParentStates ( node, toUpdate, changes );
                updateChildNodesState ( node, CheckState.checked, toUpdate, changes );
            }
        }
        else
        {
            // Removing existing mixed states
            final Iterator> iterator = nodeCheckStates.entrySet ().iterator ();
            while ( iterator.hasNext () )
            {
                final Map.Entry entry = iterator.next ();
                if ( entry.getValue () == CheckState.mixed )
                {
                    // Updating node state
                    final N node = entry.getKey ();
                    toUpdate.add ( node );
                    iterator.remove ();

                    // Saving changes
                    if ( changes != null )
                    {
                        changes.add ( new CheckStateChange ( node, CheckState.mixed, CheckState.unchecked ) );
                    }
                }
            }
        }
        repaintTreeNodes ( toUpdate );

        // Informing about state changes
        fireCheckStateChanged ( changes );
    }

    /**
     * Filters out all nodes which are children of other nodes presented in the list.
     *
     * @param nodes list of nodes to filter
     */
    protected void filterOutChildNodes ( @NotNull final List nodes )
    {
        final Iterator checkedIterator = nodes.iterator ();
        while ( checkedIterator.hasNext () )
        {
            final N node = checkedIterator.next ();
            for ( final N otherNode : nodes )
            {
                if ( isChildNode ( node, otherNode ) )
                {
                    checkedIterator.remove ();
                    break;
                }
            }
        }
    }

    /**
     * Returns whether the specified node is a child of another node or some of its child nodes or not.
     *
     * @param node   node to process
     * @param parent node to compare parent nodes with
     * @return {@code true} if the specified node is a child of another node or some of its child nodes, {@code false} otherwise
     */
    protected boolean isChildNode ( @NotNull final N node, @Nullable final N parent )
    {
        boolean childNode = false;
        if ( node != parent )
        {
            if ( parent != null )
            {
                N nodeParent = getParent ( node );
                while ( nodeParent != null )
                {
                    if ( nodeParent == parent )
                    {
                        childNode = true;
                        break;
                    }
                    nodeParent = getParent ( nodeParent );
                }
            }
            else
            {
                childNode = true;
            }
        }
        return childNode;
    }

    /**
     * Returns parent {@link MutableTreeNode} for the specified {@link MutableTreeNode}.
     *
     * @param node {@link MutableTreeNode} to find parent for
     * @return parent {@link MutableTreeNode} for the specified {@link MutableTreeNode}
     */
    @Nullable
    protected N getParent ( @NotNull final N node )
    {
        return ( N ) node.getParent ();
    }

    /**
     * Returns child {@link MutableTreeNode} for the specified parent {@link MutableTreeNode} at the specified index.
     *
     * @param parent parent {@link MutableTreeNode}
     * @param index  child {@link MutableTreeNode} index
     * @return child {@link MutableTreeNode} for the specified parent {@link MutableTreeNode} at the specified index
     */
    @NotNull
    protected N getChildAt ( @NotNull final N parent, final int index )
    {
        return ( N ) parent.getChildAt ( index );
    }

    /**
     * Returns amount of children for the specified parent {@link MutableTreeNode}.
     *
     * @param parent {@link MutableTreeNode} to count children for
     * @return amount of children for the specified parent {@link MutableTreeNode}
     */
    protected int getChildCount ( @NotNull final N parent )
    {
        return parent.getChildCount ();
    }

    /**
     * Repaints visible tree rect.
     */
    protected void repaintVisibleTreeRect ()
    {
        checkBoxTree.repaint ( checkBoxTree.getVisibleRect () );
    }

    /**
     * Repaints specified tree nodes.
     *
     * @param nodes tree nodes to repaint
     */
    protected void repaintTreeNodes ( @NotNull final List nodes )
    {
        checkBoxTree.repaint ( nodes );
    }

    @Override
    public void addCheckStateChangeListener ( @NotNull final CheckStateChangeListener listener )
    {
        listeners.add ( CheckStateChangeListener.class, listener );
    }

    @Override
    public void removeCheckStateChangeListener ( @NotNull final CheckStateChangeListener listener )
    {
        listeners.remove ( CheckStateChangeListener.class, listener );
    }

    /**
     * Informs about single or multiple check state changes.
     *
     * @param stateChanges check state changes list
     */
    public void fireCheckStateChanged ( final List> stateChanges )
    {
        if ( stateChanges != null )
        {
            for ( final CheckStateChangeListener listener : listeners.getListeners ( CheckStateChangeListener.class ) )
            {
                listener.checkStateChanged ( checkBoxTree, stateChanges );
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy