/* * Copyright 2006,2007 Enrico Boldrini, Lorenzo Bigagli This file is part of * CheckboxTree. CheckboxTree 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 2 of the License, or (at your * option) any later version. CheckboxTree 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 CheckboxTree; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA */ package net.sourceforge.pmd.jedit.checkboxtree; import java.util.HashSet; import java.util.Vector; import javax.swing.event.EventListenerList; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; /** * The default tree checking model provides: - methods for store checked * TreePaths and retrieve them. It doesn't provide the implementation of * addCheckingPath and removeCheckingPath methods (are delegated to * CheckingMode). This implementation is based on TreePath only and does not * take advantage of TreeNode convenience methods. Alternative implementation * may assume that the tree nodes be TreeNode instances. * * @author Bigagli * @author Boldrini */ public class DefaultTreeCheckingModel implements TreeCheckingModel { private HashSet<TreePath> checkedPathsSet; private HashSet<TreePath> greyedPathsSet; private HashSet<TreePath> disabledPathsSet; private PropagateCheckingListener propagateCheckingListener; protected TreeCheckingMode checkingMode; protected TreeModel model; /** Event listener list. */ protected EventListenerList listenerList = new EventListenerList(); /** * Creates a DefaultTreeCheckingModel with PropagateTreeCheckingMode. */ public DefaultTreeCheckingModel( TreeModel model ) { this.model = model; this.checkedPathsSet = new HashSet<TreePath>(); this.greyedPathsSet = new HashSet<TreePath>(); this.disabledPathsSet = new HashSet<TreePath>(); this.propagateCheckingListener = new PropagateCheckingListener(); this.setCheckingMode( CheckingMode.PROPAGATE ); } /** * @deprecated */ @Deprecated public TreeModelListener getTreeModelListener() { return null; } private class PropagateCheckingListener implements TreeModelListener { /** * Updates the check of the just inserted nodes. */ public void treeNodesInserted( TreeModelEvent e ) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenInserted( path ); } /** * Nothing to do if nodes were removed. */ public void treeNodesRemoved( TreeModelEvent e ) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenRemoved( path ); } /** * Updates the tree greyness in case of nodes changes. */ public void treeNodesChanged( TreeModelEvent e ) { TreePath path = e.getTreePath(); updateSubTreeGreyness( path ); updateAncestorsGreyness( path ); } /** * Updates the tree greyness in case of structure changes. */ public void treeStructureChanged( TreeModelEvent e ) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterStructureChanged( path ); } } /** * Updates consistency of the checking. It's based on paths greyness. */ public void updateCheckingConsistency() { updateSubTreeCheckingConsistency( new TreePath( this.model.getRoot() ) ); } /** * Updates consistency of the checking of sub-tree starting at path. * It's based on paths greyness. TODO: test this method, never used * * @param path the root of the sub-tree to be grey-updated */ public void updateSubTreeCheckingConsistency( TreePath path ) { if ( isPathGreyed( path ) ) { // greyed for ( TreePath childPath : getChildrenPath( path ) ) { updateSubTreeCheckingConsistency( childPath ); } updatePathGreyness( path ); } else { // not greyed if ( isPathChecked( path ) ) { checkSubTree( path ); } else { uncheckSubTree( path ); } return ; } } public boolean isPathChecked( TreePath path ) { return this.checkedPathsSet.contains( path ); } public boolean isPathEnabled( TreePath path ) { return !this.disabledPathsSet.contains( path ); } public boolean isPathGreyed( TreePath path ) { return this.greyedPathsSet.contains( path ); } void addToGreyedPathsSet( TreePath path ) { this.greyedPathsSet.add( path ); } void removeFromGreyedPathsSet( TreePath path ) { this.greyedPathsSet.remove( path ); } /** * Sets whether or not the path is enabled. * * @param path the path to enable/disable */ public void setPathEnabled( TreePath path, boolean enable ) { if ( enable ) { this.disabledPathsSet.remove( path ); } else { this.disabledPathsSet.add( path ); } } /** * Sets whether or not the paths are enabled. * * @param paths the paths to enable/disable */ public void setPathsEnabled( TreePath[] paths, boolean enable ) { for ( TreePath path : paths ) { setPathEnabled( path, enable ); } } void addToCheckedPathsSet( TreePath path ) { this.checkedPathsSet.add( path ); } void removeFromCheckedPathsSet( TreePath path ) { this.checkedPathsSet.remove( path ); } /** * Ungreys the subtree with root path. * * @param path root of the tree to be checked */ public void ungreySubTree( TreePath path ) { removeFromGreyedPathsSet( path ); for ( TreePath childPath : getChildrenPath( path ) ) { ungreySubTree( childPath ); } } /** * Checks the subtree with root path. * * @param path root of the tree to be checked */ public void checkSubTree( final TreePath path ) { addToCheckedPathsSet( path ); removeFromGreyedPathsSet( path ); Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); checkSubTree( childPath ); } } /** * Unchecks the subtree with root path. * * @param path root of the tree to be unchecked */ public void uncheckSubTree( TreePath path ) { removeFromCheckedPathsSet( path ); removeFromGreyedPathsSet( path ); Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); uncheckSubTree( childPath ); } } /** * Delegates to the current checkingMode the toggling style, using the * Strategy Pattern. */ public void toggleCheckingPath( TreePath path ) { if ( !isPathEnabled( path ) ) { return ; } if ( isPathChecked( path ) ) { removeCheckingPath( path ); } else { addCheckingPath( path ); } } /** * Sets the checking to path. */ public void setCheckingPath( TreePath path ) { clearChecking(); addCheckingPath( path ); } /** * Sets the checking to paths. */ public void setCheckingPaths( TreePath[] paths ) { clearChecking(); for ( TreePath path : paths ) { addCheckingPath( path ); } } /** * Clears the checking. */ public void clearChecking() { this.checkedPathsSet.clear(); this.greyedPathsSet.clear(); fireValueChanged( new TreeCheckingEvent( new TreePath( model.getRoot() ) ) ); } /** * @return The paths that are in the greying. */ public TreePath[] getGreyingPaths() { return greyedPathsSet.toArray( new TreePath[ greyedPathsSet.size() ] ); } /** * @return Returns the paths that are in the checking. */ public TreePath[] getCheckingPaths() { return checkedPathsSet.toArray( new TreePath[ checkedPathsSet.size() ] ); } /** * @return Returns the paths that are in the checking set and are the * (upper) roots of checked trees. */ public TreePath[] getCheckingRoots() { Vector<TreePath> roots = getCheckingRoots( new TreePath( this.model.getRoot() ) ); return roots.toArray( new TreePath[] {} ); } /** * @param path * @return */ private Vector<TreePath> getCheckingRoots( TreePath path ) { Object node = path.getLastPathComponent(); Vector<TreePath> roots = new Vector<TreePath>(); if ( !isPathGreyed( path ) ) { if ( isPathChecked( path ) ) { roots.add( path ); } return roots; } // path is greyed int childrenNumber = this.model.getChildCount( node ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); roots.addAll( getCheckingRoots( childPath ) ); } return roots; } /** * @return The CheckingMode. */ public CheckingMode getCheckingMode() { if ( this.checkingMode instanceof SimpleTreeCheckingMode ) { return CheckingMode.SIMPLE; } if ( this.checkingMode instanceof PropagateTreeCheckingMode ) { return CheckingMode.PROPAGATE; } if ( this.checkingMode instanceof PropagatePreservingCheckTreeCheckingMode ) { return CheckingMode.PROPAGATE_PRESERVING_CHECK; } if ( this.checkingMode instanceof PropagatePreservingUncheckTreeCheckingMode ) { return CheckingMode.PROPAGATE_PRESERVING_UNCHECK; } if ( this.checkingMode instanceof PropagateUpWhiteTreeCheckingMode ) { return CheckingMode.PROPAGATE_UP_UNCHECK; } return null; } /** * Sets the specified checking mode. The consistence of the existing * checking is not enforced nor controlled. */ public void setCheckingMode( CheckingMode mode ) { /* * CheckingMode implements togglePath method. (Strategy Pattern was * used). */ switch ( mode ) { case SIMPLE: this.checkingMode = new SimpleTreeCheckingMode( this ); break; case PROPAGATE: this.checkingMode = new PropagateTreeCheckingMode( this ); break; case PROPAGATE_PRESERVING_CHECK: this.checkingMode = new PropagatePreservingCheckTreeCheckingMode( this ); break; case PROPAGATE_PRESERVING_UNCHECK: this.checkingMode = new PropagatePreservingUncheckTreeCheckingMode( this ); break; case PROPAGATE_UP_UNCHECK: this.checkingMode = new PropagateUpWhiteTreeCheckingMode( this ); break; default: break; } // // TODO: safe to delete??? // updateTreeGreyness(); } /** * Sets the specified checking mode. The consistence of the existing * checking is not enforced nor controlled. */ public void setCheckingMode( TreeCheckingMode mode ) { this.checkingMode = mode; } /** * Adds the paths to the checked paths set * * @param paths the paths to be added. */ public void addCheckingPaths( TreePath[] paths ) { for ( TreePath path : paths ) { addCheckingPath( path ); } } /** * Adds a path to the checked paths set * * @param path the path to be added. */ public void addCheckingPath( TreePath path ) { this.checkingMode.checkPath( path ); TreeCheckingEvent event = new TreeCheckingEvent( path ); fireValueChanged( event ); } /** * Removes a path from the checked paths set * * @param path the path to be removed */ public void removeCheckingPath( TreePath path ) { this.checkingMode.uncheckPath( path ); TreeCheckingEvent event = new TreeCheckingEvent( path ); fireValueChanged( event ); } /** * Removes the paths from the checked paths set * * @param paths the paths to be removed */ public void removeCheckingPaths( TreePath[] paths ) { for ( TreePath path : paths ) { removeCheckingPath( path ); } } /** * Notifies all listeners that are registered for tree selection events * on this object. * * @see #addTreeCheckingListener * @see EventListenerList */ protected void fireValueChanged( TreeCheckingEvent e ) { // Guaranteed to return a non-null array Object[] listeners = this.listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for ( int i = listeners.length - 2; i >= 0; i -= 2 ) { if ( listeners[ i ] == TreeCheckingListener.class ) { ( ( TreeCheckingListener ) listeners[ i + 1 ] ).valueChanged( e ); } } } /** * Adds x to the list of listeners that are notified each time the set * of checking TreePaths changes. * * @param x the new listener to be added */ public void addTreeCheckingListener( TreeCheckingListener x ) { this.listenerList.add( TreeCheckingListener.class, x ); } /** * Removes x from the list of listeners that are notified each time the * set of checking TreePaths changes. * * @param x the listener to remove */ public void removeTreeCheckingListener( TreeCheckingListener x ) { this.listenerList.remove( TreeCheckingListener.class, x ); } /** * Updates the greyness value value for the given path if there are * children with different values. Note: the greyness and cheking of * children MUST BE consistent. * * @param ancestor the path to be grey-updated. */ protected void updatePathGreyness( TreePath ancestor ) { boolean value = isPathChecked( ancestor ); Object ancestorNode = ancestor.getLastPathComponent(); int childrenNumber = this.model.getChildCount( ancestorNode ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { Object childNode = this.model.getChild( ancestorNode, childIndex ); TreePath childPath = ancestor.pathByAddingChild( childNode ); if ( isPathGreyed( childPath ) ) { addToGreyedPathsSet( ancestor ); return ; } if ( isPathChecked( childPath ) != value ) { addToGreyedPathsSet( ancestor ); return ; } } removeFromGreyedPathsSet( ancestor ); } /** * Updates the greyness of sub-tree starting at path. * * @param path the root of the sub-tree to be grey-updated */ public void updateSubTreeGreyness( TreePath path ) { if ( pathHasChildrenWithValue( path, !isPathChecked( path ) ) ) { addToGreyedPathsSet( path ); } else { removeFromGreyedPathsSet( path ); } if ( isPathGreyed( path ) ) { for ( TreePath childPath : getChildrenPath( path ) ) { updateSubTreeGreyness( childPath ); } return ; } else { ungreySubTree( path ); } } /** * Updates the greyness state of the entire tree. */ public void updateTreeGreyness() { updateSubTreeGreyness( new TreePath( this.model.getRoot() ) ); } public enum ChildrenChecking { ALL_CHECKED, HALF_CHECKED, ALL_UNCHECKED, NO_CHILDREN } public ChildrenChecking getChildrenChecking( TreePath path ) { Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); boolean someChecked = false; boolean someUnchecked = false; for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); if ( isPathGreyed( childPath ) ) { return ChildrenChecking.HALF_CHECKED; } // not greyed if ( isPathChecked( childPath ) ) { if ( someUnchecked ) { return ChildrenChecking.HALF_CHECKED; } someChecked = true; } else { if ( someChecked ) { return ChildrenChecking.HALF_CHECKED; } someUnchecked = true; } } if ( someChecked ) { return ChildrenChecking.ALL_CHECKED; } if ( someUnchecked ) { return ChildrenChecking.ALL_UNCHECKED; } return ChildrenChecking.NO_CHILDREN; } /** * Note: The checking and the greyness of children MUST be consistent to * work properly. * * @return true if exists an unchecked node in the subtree of path. * @param path the root of the subtree to be checked. */ public boolean pathHasUncheckedChildren( TreePath path ) { Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); if ( isPathGreyed( childPath ) | !isPathChecked( childPath ) ) { return true; } } return false; } /** * @return true if exists a checked node in the subtree of path. * @param path the root of the subtree to be checked. */ public boolean pathHasCheckedChildren( TreePath path ) { return pathHasChildrenWithValue( path, true ); } /** * @return true if exists a node with checked status value in the * subtree of path. * @param path the root of the subtree to be searched. * @param value the value to be found. */ protected boolean pathHasChildrenWithValue( TreePath path, boolean value ) { Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); if ( isPathChecked( childPath ) == value ) { return true; } } for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { TreePath childPath = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); if ( pathHasChildrenWithValue( childPath, value ) ) { return true; } } return false; } /** * @return true if exists a child of node with a value different from * itself. * @param path the root path of the tree to be checked. */ public boolean hasDifferentChildren( TreePath path ) { return pathHasChildrenWithValue( path, !isPathChecked( path ) ); } /** * Update the grayness value of the parents of path. Note: the greyness * and checking of the other nodes (not ancestors) MUST BE consistent. * * @param path the treepath containing the ancestors to be grey-updated */ public void updateAncestorsGreyness( TreePath path ) { TreePath[] parents = new TreePath[ path.getPathCount() ]; parents[ 0 ] = path; boolean greyAll = isPathGreyed( path ); for ( int i = 1; i < parents.length; i++ ) { parents[ i ] = parents[ i - 1 ].getParentPath(); if ( greyAll ) { addToGreyedPathsSet( parents[ i ] ); } else { updatePathGreyness( parents[ i ] ); greyAll = isPathGreyed( parents[ i ] ); } } } /** * Return the paths that are children of path, using methods of * TreeModel. Nodes don't have to be of type TreeNode. * * @param path the parent path * @return the array of children path */ protected TreePath[] getChildrenPath( TreePath path ) { Object node = path.getLastPathComponent(); int childrenNumber = this.model.getChildCount( node ); TreePath[] childrenPath = new TreePath[ childrenNumber ]; for ( int childIndex = 0; childIndex < childrenNumber; childIndex++ ) { childrenPath[ childIndex ] = path.pathByAddingChild( this.model.getChild( node, childIndex ) ); } return childrenPath; } public TreeModel getTreeModel() { return this.model; } /** * Sets the specified tree model. The current cheking is cleared. */ public void setTreeModel( TreeModel newModel ) { TreeModel oldModel = this.model; if ( oldModel != null ) { oldModel.removeTreeModelListener( this.propagateCheckingListener ); } this.model = newModel; if ( newModel != null ) { newModel.addTreeModelListener( this.propagateCheckingListener ); } clearChecking(); } /** * Return a string that describes the tree model including the values of * checking, enabling, greying. */ @Override public String toString() { return toString( new TreePath( this.model.getRoot() ) ); } /** * Convenience method for getting a string that describes the tree * starting at path. * * @param path the treepath root of the tree */ private String toString( TreePath path ) { String checkString = "n"; String greyString = "n"; String enableString = "n"; if ( isPathChecked( path ) ) { checkString = "y"; } if ( isPathEnabled( path ) ) { enableString = "y"; } if ( isPathGreyed( path ) ) { greyString = "y"; } String description = "Path checked: " + checkString + " greyed: " + greyString + " enabled: " + enableString + " Name: " + path.toString() + "\n"; for ( TreePath childPath : getChildrenPath( path ) ) { description += toString( childPath ); } return description; } }